A 90+ Lighthouse score doesn't guarantee a fast website. You can score 100 on a page with no real users and 60 on one that feels instant in practice. Lighthouse is a lab test — it runs on throttled hardware with simulated network conditions — and lab scores can diverge significantly from what real users experience.
That said, the metrics Lighthouse measures do correlate with real-user experience. Improving them usually makes the site faster for everyone. The trick is knowing which changes actually move the score versus which ones are theatre.
The four scores and what they measure
Performance — the one everyone talks about. Driven by Core Web Vitals: Largest Contentful Paint (LCP), Total Blocking Time (TBT — a proxy for First Input Delay and Interaction to Next Paint), and Cumulative Layout Shift (CLS).
Accessibility — semantic HTML, contrast ratios, ARIA labels, keyboard navigation. Easier to fix than performance, often neglected.
Best Practices — HTTPS, no console errors, no deprecated APIs, secure third-party scripts.
SEO — meta tags, crawlability, structured data. Mostly mechanical fixes.
Accessibility, Best Practices, and SEO can all reach 100 with focused effort and no architectural changes. Performance is harder — it requires either genuinely fast assets or a fast hosting environment.
What actually moves Lighthouse Performance
Largest Contentful Paint (LCP)
LCP measures how long the largest visible element takes to appear. It's usually a hero image or a large text block. Target: under 2.5 seconds.
The single biggest lever: image optimisation. Uncompressed hero images are the most common LCP killer. Use modern formats (WebP, AVIF), add explicit width and height attributes, and fetchpriority="high" on the LCP image so the browser prioritises it.
In Next.js, the <Image> component handles WebP conversion, lazy loading, and fetchpriority automatically — use it for every image that appears above the fold.
Second biggest: server response time. If your TTFB (Time to First Byte) is over 600ms, LCP will suffer regardless of what you optimise client-side. Cloudflare Pages and Vercel both serve from edge nodes close to the user — switching from a traditional server to edge hosting can cut TTFB by 400ms+ for geographically dispersed users.
Preload the LCP resource. If the browser discovers your hero image late (buried in CSS or JS), it'll start fetching it too late:
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
Total Blocking Time (TBT)
TBT measures how long the main thread is blocked by JavaScript — preventing user interactions from being processed. Target: under 200ms.
Code split aggressively. Every JavaScript file that isn't needed for the initial render is blocking time you're paying for unnecessarily. Next.js does route-based code splitting by default. Go further: lazy-load heavy components that aren't above the fold.
const HeavyChart = dynamic(() => import("./HeavyChart"), { ssr: false });
Defer third-party scripts. Analytics, chat widgets, and ad scripts are TBT killers. Load them with strategy="lazyOnload" in Next.js, or add defer and async attributes manually. Never load them synchronously in <head>.
Audit your JavaScript bundle. Use @next/bundle-analyzer to see what's in your bundle. One large dependency you don't need can add 50ms of blocking time.
Cumulative Layout Shift (CLS)
CLS measures visual instability — elements jumping around as the page loads. Target: under 0.1. It's the easiest metric to fix once you know what causes it.
Always set explicit dimensions on images. Without width and height, the browser doesn't know how much space to reserve and the layout shifts when the image loads.
Reserve space for dynamically loaded content. If you're loading content asynchronously (ads, banners, embeds), give the container a fixed height or min-height before the content arrives.
Use font-display: optional for web fonts. Fonts that load after text is rendered cause layout shift when the fallback swaps. optional tells the browser to only use the font if it's available immediately (i.e., cached). For critical fonts, preload them.
Accessibility (the quick wins)
- Every
<img>needs analtattribute — empty string for decorative images, descriptive text for meaningful ones - Interactive elements (buttons, links) need discernible text — icon-only buttons need
aria-label - Heading hierarchy must be sequential — no jumping from
<h1>to<h3> - Colour contrast: 4.5:1 for normal text, 3:1 for large text — check with the Chrome DevTools colour picker
- Form inputs need associated
<label>elements
Running the Lighthouse audit will list every failure with a link to the specific element in the DOM. Fix them systematically.
The changes that moved our scores
On a recent client project starting in the low 60s for Performance:
| Change | LCP improvement | TBT improvement |
|---|---|---|
| Hero image → WebP + preload | −1.1s | — |
| Cloudflare Pages (edge hosting) | −0.4s TTFB | — |
| Bundle split: deferred analytics | — | −180ms |
| Explicit image dimensions | — | — (CLS fixed) |
| <Image> component for all above-fold images | −0.3s | — |
Final score: 96 Performance, 100 Accessibility, 100 Best Practices, 100 SEO.
The analytics deferral was the most impactful single change for TBT. Third-party scripts are routinely the biggest performance liability on otherwise well-built sites.
What Lighthouse doesn't measure
Field data — what real users experience on real devices and networks — is captured by the Chrome User Experience Report (CrUX) and shown in Google Search Console under Core Web Vitals. A site can score 95 in Lighthouse and have "Poor" field LCP if most users are on slow mobile connections in high-latency regions.
Track both. Lighthouse tells you what to fix. Field data tells you whether it worked.
We audit and optimise web app performance as part of our web services. If your site scores below 80 and you're not sure where to start, get in touch.