Web Core Vitals: how I ship a 100/100 Lighthouse score (and keep it)
A practical, no-nonsense recipe for Lighthouse 100s on Next.js sites. The exact image, font, and JS choices I make on every project.
A 100/100/100/100 Lighthouse score is not magic. It is a checklist. Here is mine β the same one I use on every Next.js site, including this one.
The recipe
Images
next/imagefor everything. No raw<img>tags.- AVIF + WebP via
next.config.ts(formats: ["image/avif", "image/webp"]). priorityon the LCP image only. Do not over-prioritise.- Real
width+heightto avoid CLS. - For decorative SVGs, inline them. For complex SVGs,
next/image.
Fonts
next/font/google(ornext/font/local) β never CDN linked.display: "swap"is fine.display: "optional"is faster but skips your font on slow connections (often a worthy trade).- Subset to the languages you actually use.
- Preload the variable font once, not the static cuts.
JavaScript
- App Router + RSC by default. Client components only when they need state or browser APIs.
dynamic()import for heavy components below the fold (charts, code editors, video players).optimizePackageImportsinnext.config.tsforlucide-react,framer-motion,date-fnsetc.- No heavy state libraries unless needed.
useState+ Server Actions cover 80%.
CSS
- Tailwind v4 β JIT, ~12kb gzipped on a typical site.
- One global stylesheet, zero CSS-in-JS runtime libs.
font-familydeclared in<html>, not per component.
Third-party scripts
- Defer everything.
next/script strategy="lazyOnload"orworker. - Analytics: Plausible, Vercel Analytics, or self-hosted Umami. Avoid GA4 if you can β it is heavy.
- Chat widgets: load on hover/click of the trigger, not on page load.
Caching & ISR
force-staticwhere possible.- ISR with
revalidatefor content that changes (blog, products). - Edge runtime for middleware and lightweight APIs.
generateStaticParamsfor all known routes (services, country pages, etc.).
What kills scores most often
In order of frequency:
- A single un-optimised hero image. Convert to AVIF, set
priority, set width/height. - A chat widget loading on every page. Lazy-load on click.
use clienteverywhere. Audit your tree. Most components are server.- Google Fonts via CSS link. Switch to
next/font. - Third-party CSS frameworks loading on top of Tailwind. Pick one.
The "100/100 audit" I run before launch
# 1. Build production
npm run build && npm run start
# 2. Lighthouse via CLI
npx lighthouse http://localhost:3000 --view --preset=desktop
npx lighthouse http://localhost:3000 --view --preset=mobile
# 3. Check the bundle
npx @next/bundle-analyzer
# 4. Check images
# Look for any image > 200kb in /_next/image responses
Common findings:
- Hero image is 800kb β re-export at correct size, AVIF.
- 250kb of JS for a static landing page β some library is bundled when it shouldn't be.
- Layout shifts on font load β preload the variable font, use
font-display: swap.
Maintaining 100s after launch
Scores drift. Editorial uploads a 4MB PNG. A new feature pulls in a 200kb library. Set up:
- Lighthouse CI in GitHub Actions β run on every PR, fail the build if perf < 90.
- Vercel Speed Insights β real-user data, watch the median over time.
- A budget: total JS < 200kb gzipped, total images < 500kb on the LCP path.
The honest limit
You will not get 100/100 on a page with:
- Embedded YouTube/Vimeo
- A live chat widget that loads on every page
- Heavy ad networks
- A CMS that injects unoptimised inline scripts
That is fine. 95+ is the practical target. 100 is the LinkedIn screenshot.
TL;DR
next/image + next/font + Tailwind + RSC by default + lazy-load third parties + Lighthouse CI. That is the recipe. It works on every site I ship.
If your Next.js site is stuck at 60-80, tell me what you are building β usually I can spot the top three fixes in a 15-minute review.
Author
Usama
I have spent the last 6+ years shipping production websites and apps from Pakistan for clients across 30+ countries. I work daily on Fiverr and Upwork, and partner directly with founders, agencies and local businesses on long-term builds.
More about Usama