Core Web Vitals at Scale: Lessons from 30+ Production Sites
The Real Problem with WooCommerce Performance
After optimizing 30+ production WooCommerce sites, I've learned that the standard advice — "enable caching, compress images, use a CDN" — gets you to a 70 Lighthouse score. Getting to 90+ requires a fundamentally different approach.
Here's what actually works at scale.
Critical CSS Inlining
The single biggest LCP win on static Next.js sites is eliminating render-blocking CSS. With the critters library in the postbuild step, above-the-fold styles are inlined into the HTML and the full stylesheet is loaded asynchronously.
// next.config.ts
experimental: {
optimizeCss: true, // uses critters internally
}
For custom implementations, a postbuild script using Critters directly gives more control over which routes get inlined and what threshold to use for "above the fold".
Image Pipeline: WebP at Build Time
Rather than relying on Next.js Image's on-demand optimization (which adds latency on first load), I pre-convert all screenshots and hero images to WebP at build time using Sharp:
// scripts/convert-screenshots.mjs
await sharp(src)
.resize(960, undefined, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(dest);
This runs as a prebuild script. WebP files are gitignored (generated artifacts), PNGs stay in source control.
Font Loading: The Swap vs Block Tradeoff
Using next/font with display: 'swap' eliminates FOIT but can cause CLS if the fallback font has different metrics. The fix: use adjustFontFallback: false and manually set size-adjust in the font-face fallback.
For monospace fonts used in code blocks, preload: false avoids an unnecessary preload hint for a font that's only visible after scroll.
Eliminating Third-Party JS
Every third-party script — analytics, chat widgets, cookie banners — adds to INP. On the portfolio itself, there is zero third-party JavaScript. Analytics can wait; the user experience cannot.
For client sites where analytics is non-negotiable, next/script with strategy="afterInteractive" defers execution until after the page is interactive.
Measuring What Matters
Lighthouse scores are a proxy. The real metric is field data from CrUX. A page can score 95 in Lighthouse and still have poor field LCP if the server is slow for real users.
The workflow: optimize locally with Lighthouse, verify with PageSpeed Insights (which uses real CrUX data), then monitor with Core Web Vitals in Search Console.
