How I built this portfolio for a perfect performance score
10 July 2026 · 3 min read
Most developer portfolios claim to be "fast." This one measures its own Core Web Vitals in your browser and shows you the numbers in the hero. Here's how it's built to earn them.
Answer first: what actually moves the needle
If you only take three things away:
- Render HTML, not JavaScript. React Server Components send finished markup, so the page is useful before any hydration.
- Lazy-load anything heavy. The 3D hero and animation libraries are dynamically imported and never sit in the first load.
- Be ruthless about fonts and images. They are the two biggest, most common Core Web Vitals regressions.
RSC-first rendering
The App Router lets most of the page be a Server Component. That means the content (headings, copy, project cards) is HTML by the time it reaches the browser. Only genuinely interactive pieces (the command palette, the theme toggle, the 3D canvas) opt into client JavaScript with "use client".
The practical rule I follow: a component is a Server Component until it needs state, effects, or browser APIs. Keeping that boundary tight is the single biggest lever on bundle size.
3D that never blocks
A React Three Fiber scene is lovely, but Three.js is not small. So the hero scene is:
- Dynamically imported with
ssr: false, so it's never part of the server response or the initial bundle. - Gated on
prefers-reduced-motion: if you prefer reduced motion, it isn't mounted at all; a static gradient stands in. - Capped at a sensible device-pixel-ratio with a low-poly geometry, so even when it runs, it's cheap.
The result: the 3D is a progressive enhancement, not a tax on the people who can least afford it.
Fonts and images
Fonts are self-hosted with next/font, subset to Latin, and limited to the weights actually used, with font-display: swap and size-adjusted fallbacks so there's no layout shift when the web font loads.
Images go through next/image with explicit dimensions (no CLS), modern formats, and lazy-loading below the fold. The one image that is the Largest Contentful Paint element gets priority; nothing else does.
Prove it, don't claim it
The hero shows your real LCP, INP, and CLS using the web-vitals library. It's about 2 KB, deferred, and reserves its own space so it can't cause layout shift. That's the whole philosophy of this site: don't tell people you're a performance engineer; let them watch the proof on their own device.
Takeaways
- Default to Server Components; make the client boundary earn its place.
- Treat every dependency's weight as a budget line.
- Measure real field data, not lab screenshots.
Got a project like this?
I build the things I write about.