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)