Turning a Red Lighthouse Score Green: A React Playbook
The concrete, highest-ROI changes that move React Lighthouse and Core Web Vitals scores — image handling, code splitting, main-thread work, and font loading — in the order I actually do them.
The short answer: the biggest Lighthouse wins for a React app, in order, are: fix your images (format, size, dimensions, priority), ship less JavaScript (code-split by route, lazy-load heavy libs), get work off the main thread, and preload fonts. Chase those four before anything clever and most red scores go green.
A low Lighthouse score is rarely one big problem — it's a handful of well-known issues that every React app accumulates. I've turned enough red dashboards green to know the fixes come in a reliable order of return on effort. Here's the playbook, highest-ROI first. (Measure before you start — Lighthouse is a lab score; pair it with real field data on Core Web Vitals.)
1. Images — almost always the biggest LCP win
Your Largest Contentful Paint element is usually an image, and images are usually the biggest, laziest win on the board:
- Serve modern formats (AVIF/WebP) and right-size them — don't ship a 3000px hero to a 400px phone.
- Always set width/height (or
aspect-ratio) so the browser reserves space — this kills the layout shift that wrecks CLS. - Mark the LCP image
priorityso it preloads instead of lazy-loading.
2. Ship less JavaScript — the TBT / INP win
Lighthouse punishes Total Blocking Time, and TBT is JavaScript executing on the main thread. The fixes, in order:
- Code-split by route with
React.lazy+Suspenseso the home page doesn't ship the dashboard's bundle. - Lazy-load heavy libraries (charts, editors, date pickers) behind the interaction that needs them.
- Audit the bundle with a visualizer and cut the surprises — a date library pulling every locale, two libraries doing one job, a 70KB dependency used once.
The fastest code is the code you never shipped; a lean bundle moves TBT, INP, and First Contentful Paint all at once.
3. Get work off the main thread
Long tasks block input and hurt INP (now a Core Web Vital). Break up heavy synchronous work: defer non-urgent updates with startTransition, debounce expensive handlers, and virtualize long lists so you render dozens of rows instead of thousands.
4. Fonts — a quick FCP and CLS win
Web fonts block text rendering and cause layout shift when they swap in. font-display: swap (or a framework font loader) renders text immediately in a fallback, and preloading the font file closes the gap. Next.js's next/font self-hosts and preloads automatically — a near-free win.
5. Render strategy — the structural lever
For a content or marketing site, server-render or statically generate the HTML so the user sees content before any JS runs; hydrate only what's interactive. This is the difference between a blank screen waiting on a bundle and a page that paints instantly. It's more work than the first four, but it raises the ceiling on what's achievable.
The order matters
Do images and bundle size first — they're the biggest movers for the least effort, and they lift LCP, FCP, TBT, and CLS together. Then main-thread work for INP, then fonts, then render strategy if you still need more. Re-run Lighthouse after each change so you know what actually moved, and anchor it to field data — a green lab score that users don't feel is a vanity metric. The goal isn't the number; it's the page feeling instant on a mid-range phone on bad wifi. Fix it there and the number follows.