Starting Point: PageSpeed 47
After deploying my portfolio, I ran PageSpeed Insights for the first time. Mobile score: 47. Desktop: 71. SEO: 100. The SEO was solid (thanks to structured data and proper metadata), but performance was red — embarrassing for a developer portfolio.
The three main culprits: CLS of 0.872, render-blocking Google Fonts (104KB), and S3 project images served as full-resolution PNGs.
Fix 1: Self-Host Fonts with next/font
Google Fonts was my biggest render blocker. Every page load waited for fonts.googleapis.com to respond before rendering text. The fix was switching to next/font:
// BEFORE — in layout.tsx <head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"/>
// AFTER — at the top of layout.tsx
import { Inter, Space_Grotesk } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
weight: ['400', '500', '600', '700'], // removed unused weights
})This eliminated the external request entirely. Next.js downloads the font at build time, self-hosts it, and inlines a font-face declaration. FCP improved by ~800ms.
Fix 2: CLS from GSAP Animations
My about section had a CLS of 0.872 — nearly 9x the "good" threshold of 0.1. The cause: GSAP was setting opacity: 0 on elements before animating them in. When these elements had no reserved dimensions, the browser didn't allocate space for them — then they popped in and shifted everything.
/* Add to your critical inline CSS in layout.tsx */
.about-section {
min-height: 100vh; /* reserves space before GSAP runs */
}
.about-content,
.about-visual {
min-height: 400px; /* prevents collapse to 0 */
}
/* Prevent text spans from collapsing */
.reveal-word {
display: inline-block;
min-width: 1ch;
}Fix 3: Image Optimization
Project screenshots from S3 were being served as full 2560px PNGs. I made two changes:
// next.config.js
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'], // auto-convert
minimumCacheTTL: 31536000, // 1 year cache
},
}
// In Hero.tsx — proper sizes and priority
<Image
src={item.src}
alt={item.title}
fill
priority={index < 3}
fetchPriority={index === 0 ? 'high' : undefined}
loading={index < 3 ? 'eager' : 'lazy'}
sizes="(max-width: 768px) 50vw, 20vw"
/>Results
- Mobile Performance: 47 → ~75
- Desktop Performance: 71 → ~90
- FCP: 3.6s → 0.8s
- CLS: 0.872 → under 0.1
- SEO: 100 (unchanged)
The best optimization is the one you actually ship. Pick the 3 highest-impact items and do those first.