Back to blogs

How I Took Core Web Vitals from 65 to 99 in Next.js

Jul 13, 2026 · 3 min read

Next.jsCore Web VitalsPerformance
How I Took Core Web Vitals from 65 to 99 in Next.js

"Improve Core Web Vitals" is easy to say and vague to execute. On the Hyqoo rebuild, the score moved from an average of 65 to 99, and it came down to a short list of specific, unglamorous changes — not one silver bullet.

Start with what the score is actually measuring

Core Web Vitals boils down to three things: how fast the largest visible element loads (LCP), how fast the page responds to the first interaction (INP, formerly FID), and how much the layout jumps around while loading (CLS). Every change below maps to one of those three.

1. Static HTML by default (LCP)

If the browser has to wait on a database query, an API call, and a render pass before it can paint anything, LCP suffers no matter how "optimized" the components are. Making every page statically generated means the browser gets fully-formed HTML immediately — there's no server-side work standing between the request and the first paint.

2. Images: right format, right size, right priority (LCP)

Next.js's <Image> component handles responsive sizing and modern formats automatically, but it only helps if it's used correctly:

<Image
  src={logo}
  width={65}
  height={65}
  priority // only for the actual above-the-fold image
/>

The mistake I'd made earlier in this same portfolio project, before fixing it: a shared image component had priority hardcoded to true for every image, including ones far below the fold. That tells the browser to eagerly fetch images nobody's about to see, competing for bandwidth with the image that actually determines LCP. Making priority opt-in, and reserving it for the one real above-the-fold image, is a bigger win than it sounds like.

3. Ship less JavaScript, and load what's left lazily (INP)

Every animation library, every client component, every third-party script adds parse-and-execute time before the page is actually interactive. The fixes that mattered:

  • Server Components by default; "use client" only where interactivity is genuinely needed.
  • Heavy client-only widgets (a markdown editor, for instance) loaded via next/dynamic with ssr: false, so they don't block the initial render at all.
  • Respecting prefers-reduced-motion and keeping animation logic minimal rather than animating everything by default.

4. Reserve space before content arrives (CLS)

Layout shift almost always comes down to not telling the browser how much space something will take before it loads. Explicit width/height on every image, fixed-size containers for anything that loads asynchronously, and fonts loaded via next/font (which avoids the flash-of-invisible-text reflow you get from a naive Google Fonts <link> tag) — all of it comes back to giving the browser enough information up front to not have to guess.

5. Security and SEO tags that don't cost render time

CSP headers, security headers, and JSON-LD structured data all get set at the response/build level — none of it is client-side JavaScript computing and injecting tags after the page has already loaded. It's "free" from a Core Web Vitals perspective precisely because it never touches the render path.

The actual takeaway

None of these five things individually moves a score from 65 to 99. It's the combination — and specifically, removing the unnecessary work (eager-loading images nobody sees yet, shipping JS for components that don't need to be interactive, letting fonts reflow the page) rather than adding anything clever. Most performance wins are subtraction, not addition.