PerformanceNext.jsUX

A Practical Guide to Core Web Vitals

Alejandro Gómez12 min read

The techniques that moved a real e-commerce storefront from a 54 to a 91 Lighthouse score — fonts, images, render blocking, and LCP.

What the scores actually measure

Core Web Vitals are three metrics: LCP (Largest Contentful Paint — how fast the page looks loaded), CLS (Cumulative Layout Shift — how stable the layout is), and INP (Interaction to Next Paint — how responsive the page is to input).

A Lighthouse score is a weighted blend of these and a few secondary metrics. But in practice, fixing LCP and CLS accounts for 80% of the score improvement on most sites.

LCP: the image problem

The LCP element is almost always the hero image or the largest text block above the fold. For images, the fix is almost always the same: use a modern format (WebP or AVIF), preload the LCP image, and size it correctly so the browser doesn't download a 3000px image for a 400px slot.

// Preload the LCP image in Next.js
<Image
  src="/hero.jpg"
  priority       // adds <link rel="preload">
  sizes="100vw"
  fill
  alt="Hero image"
/>

LCP: the font problem

Web fonts block the LCP text from rendering. The solution is to preconnect to the font provider, use font-display: swap, and — if possible — self-host the font so you control the cache headers.

  • Add <link rel="preconnect"> for the font origin
  • Use font-display: optional if layout shift from swap is visible
  • Self-host via next/font/google which inlines the @font-face at build time
  • Subset the font to only the characters you use

CLS: reserve space for async content

Layout shifts happen when content loads asynchronously and pushes other elements around. The fix is to always reserve space before the content arrives — set explicit width and height on images, use min-height on ad slots, and avoid inserting content above the fold after load.

The render-blocking JS problem

Third-party scripts (analytics, chat widgets, A/B testing) are the biggest culprit for render-blocking. Load them with strategy="lazyOnload" in Next.js Script, or defer them until after the first user interaction.

Performance is not a one-time audit. It's a constraint you design around from the start. Adding it later is an order of magnitude more expensive than building it in.