DEV Community

Cover image for Why Your Lighthouse Score Isn’t Moving (and What Actually Helped)
John Munn
John Munn

Posted on

Why Your Lighthouse Score Isn’t Moving (and What Actually Helped)

The Lighthouse Mirage

You’ve followed every blog post, checked every box. Lazy loading, bundle trimming, third-party cleanup, and your Lighthouse score still won’t budge. That’s exactly where we were.

After a few weeks of frustration, we stopped guessing and got methodical. Here’s what actually moved the needle, and what just made us feel productive without real gains.


What Didn’t Work (Or Didn’t Matter Much)

Lazy-loading everything

We thought it would save bandwidth, but we inadvertently delayed key LCP elements. If your hero image is lazy-loaded, your LCP tanks.

Swapping to AVIF or WebP

Great in theory, but without width/height or aspect-ratio, layout shifts killed CLS gains.

Killing spinners for skeletons

Skeletons looked slick, but FCP didn’t improve because JS was still evaluating too late. User perception barely changed.

Micro-optimizing JS dependencies

Replacing a 30kb library with a 25kb one didn’t matter. The main thread was still blocked by hydration and analytics.


What Actually Helped

1. Breaking Up the Main Thread

We used the Chrome DevTools Performance tab to identify long tasks (3s+). It turns out our initial hydration was locking up the main thread. We wrapped hydration logic for less-critical widgets in requestIdleCallback and deferred some client-only effects to fire after TTI.

Combined with React.lazy and dynamic import boundaries, this allowed the browser to prioritize interactivity sooner.

Before: Main thread locked for 3.2s
After: Long tasks split, TTI improved by 700ms
Final result: TTI dropped from 3.2s → 2.5s

2. Preconnect & Preload

Preconnecting to Google Fonts, our CDN, and analytics domains shaved ~200ms off TTFB.

We also preloaded the main hero image and critical CSS. The position of those links in the <head> mattered more than we expected.

Here’s a real example: We had a key landing page where everything seemed optimized — minified CSS, third-party widgets removed, offscreen images lazy-loaded. Yet LCP was still stuck at 4.1s. Adding a single rel="preload" hint for the hero image dropped it to 2.8s overnight.

3. Performance Budgets

We enforced real performance budgets in CI using Lighthouse CI and bundle analyzer thresholds. Anything that exceeded the max budget would fail the pipeline, prompting the dev to trim or split the bundle.

Our budget: 170kb for main.js and 100kb for above-the-fold CSS.

4. Layout Stability Over Image Format

We got more CLS improvement by adding fixed width and height attributes than we did by switching image formats. aspect-ratio was a huge win for blog cards, product tiles, and dynamic thumbnails.

5. Controlled Hydration

We avoided rendering everything eagerly. Instead:

  • Used requestIdleCallback to hydrate chat widgets, recommendation carousels, and tracking scripts
  • Disabled non-essential components entirely for mobile

One simple mobile rule: No third-party scripts under 400px width.


Tools That Helped Us See Clearly

  • WebPageTest and SpeedCurve to correlate real-world LCP and CLS
  • Chrome DevTools > Performance Tab to analyze long tasks
  • Lighthouse CI + GitHub Actions to catch regressions early

Takeaway: Stop Optimizing for Scores

Lighthouse is a great tool, but it’s not the goal.

We stopped chasing a 100 and started measuring what users actually feel. Mobile TTI. Real layout shifts. Main thread locks.

That’s when performance actually improved, and so did our metrics.


Have any tips on how to improve Lighthouse scores? Drop them in the comments below!

Top comments (0)