A 95+ Lighthouse score isn't a vanity metric — Core Web Vitals are a ranking signal and a proxy for how fast the experience actually feels. In Next.js the wins are well understood; you just have to apply them consistently.
Which rendering strategy fits which page?
Match the strategy to how often content changes and who writes it:
- Static (SSG) for marketing pages and blog posts — fastest possible delivery, zero server cost per view.
- Incremental Static Regeneration for pages that change occasionally — product listings, docs, portfolio case studies.
- Streaming SSR for personalised shells — send the static frame instantly, stream user-specific widgets as they resolve.
- Pure client rendering only where data is genuinely real-time and behind auth — dashboards, editors.
Which mistakes keep Next.js scores low?
- Marking a whole page 'use client' because one widget needed state — the entire page's JS then ships to the browser.
- Raw <img> tags without dimensions, causing layout shift that destroys CLS after hydration.
- Third-party chat/analytics widgets loaded synchronously in <head>, stalling first paint for everyone.
- Fetching data in useEffect on mount instead of on the server — waterfall requests plus a spinner users actually see.
- Importing icon/UI libraries through barrel files so hundreds of unused components ride along in the bundle.
None of these are exotic — they're defaults that crept in under deadline. The fix is architectural hygiene enforced by review: a one-line PR checklist ('new client component? new image? new third-party script?') catches every item above before it merges.
Why render on the server by default?
Server Components ship zero JavaScript for everything that isn't interactive. Keep 'use client' at the leaves of the tree — a single button, a carousel — not at the top of a page, so the bulk of your UI stays static HTML.
How do you win LCP with images and fonts?
- Use next/image with explicit dimensions to reserve space and kill layout shift (CLS).
- Mark the hero/LCP image priority so it isn't lazy-loaded.
- Self-host fonts with next/font and font-display: swap to avoid invisible text and network round-trips.
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});How do heavy animations hurt real users?
Heavy animation and 3D can tank performance on mid-range devices. Cap device pixel ratio, lazy-load WebGL canvases below the fold, and honor prefers-reduced-motion so the experience degrades gracefully.
Performance is a feature you ship every release — measure it in CI, not once before launch.
What should you measure, and where?
- Lab data in CI: run Lighthouse on every deploy preview with budgets for LCP, CLS and total JS; fail the build on regression.
- Field data from users: INP only shows up in real-usage data (Chrome UX Report or your RUM snippet) because it depends on actual interaction.
- Bundle size per route: track the client-component share of each page — growth there is the earliest warning of future INP problems.
The workflow that keeps scores high is unglamorous: measure before refactoring, ship one change at a time, re-measure, and encode every hard-won win as an automated budget. Teams that skip the last step watch their 98 drift back to 84 within two quarters.
One caveat worth internalising: lab scores and field experience diverge most on interaction metrics. A perfect Lighthouse run says nothing about how long a real checkout button takes to respond on a three-year-old Android — only RUM (or CrUX once your traffic qualifies) captures that. Treat the lab as your regression net and the field as your ground truth, and investigate the moment they disagree.
Wire Lighthouse into your pipeline with a budget, and regressions get caught in review instead of by users.