Monday morning. Lighthouse CI fails on the staging deploy. Performance score: 76. Last week: 96. No one pushed code over the weekend. No dependency updates. No config changes.
I spent 8 hours tracking this down. Here's the debugging path.
Hour 1: The obvious checks
First thought: flaky test. Lighthouse scores vary by ±3–5 points between runs due to network and CPU conditions. A 20-point drop isn't variance — it's a regression.
I ran Lighthouse locally: 94. Close enough to the baseline. Not a regression in the code.
I ran Lighthouse on the deployed staging URL: 76. Consistent with CI.
The gap between local and deployed told me the issue was infrastructure, not code.
Hour 2: Waterfall analysis
I opened Chrome DevTools → Performance tab on the staging URL. The waterfall showed:
- HTML: 180ms (normal)
- Main JS bundle: 120ms (normal)
- Font file: 2,100ms (not normal — usually 80ms)
- Hero image: 350ms (normal)
The font file was loading from a different CDN than usual. I checked the <link> tag — it pointed to Google Fonts via fonts.googleapis.com. That hadn't changed. But the response time had.
I curl'd the font URL from the Vercel edge location:
curl -o /dev/null -w "%{time_total}" https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700
2.3 seconds. From my local machine: 45ms.
Hour 3: The font detour
Google Fonts was slow from the Vercel edge. Why? I checked Vercel's edge function logs. The staging deployment was in the iad1 (Virginia) region. Google Fonts serves from the nearest CDN node. But the Lighthouse CI runner was in a different region — and the edge function was making a cross-region request to Google Fonts.
Wait — fonts should be cached on the CDN. I checked the response headers:
cache-control: private, max-age=86400
private means the CDN can't cache it. Only the browser can. On a fresh Lighthouse run (empty cache), every font request goes to origin. And from the CI runner's location, origin was slow.
Fix: Self-host the font files. Download the WOFF2 files, put them in /public/fonts/, and reference them with @font-face in CSS.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
After deploying self-hosted fonts: font load time dropped from 2,100ms to 40ms. Lighthouse score: 88. Better — but still not 96.
Hour 5: The image that wasn't lazy
8 points still missing. I ran Lighthouse with the "Treemap" view. The LCP element was the hero image — correct. But the LCP time was 2.8s, up from 1.2s.
I checked the hero <Image> component:
<Image src="/hero-bg.webp" alt="..." fill priority sizes="100vw" />
priority means loading="eager" and adds a preload hint. This was correct — the hero image should load eagerly. So why was LCP slow?
I checked the network tab. The hero image loaded fine. But a second large image below the fold was also loading eagerly:
// About section — below the fold
<Image
src="/about-photo.webp"
alt="..."
width={600}
height={800}
priority // ← this was the problem
/>
Someone had added priority to the about section photo during a content update. This told the browser to preload two large images simultaneously. The hero image competed for bandwidth with the about photo, increasing LCP by ~1.5 seconds.
Fix: Remove priority from the below-fold image.
<Image
src="/about-photo.webp"
alt="..."
width={600}
height={800}
// No priority — let the browser lazy-load it
/>
After deploying: LCP dropped to 1.3s. Lighthouse score: 95. Close enough to the 96 baseline.
Hour 7: Why CI caught it but local didn't
Local Lighthouse runs at full bandwidth and low latency. The font and image issues were hidden by fast local connections. The CI runner simulates a throttled mobile connection (4G), where the bandwidth competition between two priority images and a slow font actually mattered.
This is exactly why you run Lighthouse in CI, not just locally.
Hour 8: Preventing it from happening again
I added three guards:
- Lighthouse CI budget in
.lighthouserc.js:
ci: {
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
},
},
},
This fails the CI check if performance drops below 90 or LCP exceeds 2.5s. No more silent regressions.
-
ESLint rule for
priorityon images: A comment in the Image component documenting that only one image per page should havepriority. -
Self-hosted fonts as a project rule: No more Google Fonts links. All fonts vendored into
/public/fonts/.
The timeline
| Time | Action | Score |
|---|---|---|
| 0h | Noticed CI failure | 76 |
| 2h | Identified Google Fonts latency from edge | 76 |
| 4h | Self-hosted fonts | 88 |
| 6h | Removed extra priority from below-fold image | 95 |
| 8h | Added CI budgets + prevention rules | 95 (guarded) |
20-point drop. Two root causes, both invisible locally. 8 hours to find, 10 minutes to fix. That's performance debugging.
Seeing performance regressions you can't explain? Let's talk — I run Lighthouse 98 on production sites and can audit your performance pipeline.