Performance work has a hype-to-impact ratio problem. Half the advice out there is about micro-optimizations that shave milliseconds off a page that was never the bottleneck. The other half is genuinely load-bearing and gets buried under it. Here's the short list, in the order I'd actually do it again on a new project.

1. Serve images at the size they're displayed, not the size they were uploaded

This is the single highest-leverage change on most content-heavy sites, and it's almost always skipped first because it feels like the least interesting fix.

I had a hero photo on this site sitting at 1024×1536 — roughly 2MB — being displayed in a box that's never wider than 384px on any screen. Every visitor was downloading four times the pixels they'd ever see.

The fix is next/image with a real sizes attribute instead of a raw <img> tag:

<Image
  src='/images/me3.png'
  alt='Alireza Mohseni'
  fill
  priority
  className='object-cover'
  sizes='(max-width: 1024px) 320px, 384px'
/>

next/image resizes, re-encodes to AVIF/WebP where supported, and serves the right size per breakpoint automatically — the sizes attribute is the part people skip, and it's the part that actually tells the browser which size to request. Without it, you can still ship the largest variant to every device.

2. Don't ship a component library's worth of icons for five icons

It's easy to end up importing a whole icon package for a handful of glyphs. The fix isn't exotic — named imports from a tree-shakeable package, and check your bundle analyzer actually agrees it's tree-shaking before assuming it is. I've been burned by icon packages that claim ESM support but still pull in the full set under certain bundler configs.

3. Static generation over client-side data fetching, wherever the data allows it

Every page on this site is statically generated at build time — no client-side fetch waterfall, no loading spinner, no layout shift while data resolves. That's not a performance trick, it's just picking the right rendering strategy for content that doesn't change per request. The trap is defaulting to client components out of habit and then having to claw performance back later.

4. Fonts: subset, display: swap, and stop there

next/font handles subsetting and self-hosting automatically, which removes the biggest font-related performance mistake (loading from a third-party CDN with its own connection overhead) for free. Past that, I stopped optimizing fonts — the marginal gains weren't worth the complexity budget on a site this size.

What I didn't bother with

Aggressive code-splitting beyond what Next.js does by default, and manually inlining critical CSS. Both are real techniques with real use cases, but on a mostly-static personal site, the cost-benefit wasn't there. The lesson isn't "do every optimization" — it's "measure first, and fix the thing that's actually large before the thing that's merely interesting."