Back to articles
PerformanceReactLighthouse

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.

Yash Thakur
4 min read
Turning a Red Lighthouse Score Green: A React Playbook

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 priority so 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 + Suspense so 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.

Written by Yash Thakur

Senior React Developer · 8+ years building for the web

More articles

Keep reading