Why Lighthouse Scores Lie and Still Matter for Search
Uptime reported 99.99% healthy. Lighthouse came back Performance 92, Accessibility 100, Best Practices 93, SEO 98. The dashboard said “fine” while search traffic was already taking damage. Here's how a green score masked a rendering failure that cost organic clicks overnight.
On This Page
The Lie
92
Performance
100
Accessibility
93
Best Practices
98
SEO
Uptime reported 99.99% healthy. Every synthetic check passed. The dashboard said everything was fine.
The Truth
We shipped a render change that returned a 200 and an 8.42KB HTML shell. The client bundle failed to hydrate on 83.1% of requests because of an async import race.
8.42KB
HTML shell (was 42.7KB)
14.3s
Field LCP
7.1s
INP
-0.37%
Organic clicks overnight
Users got a blank main pane. Crawlers indexed empty bodies. Your green monitor was gaslighting you.
Executive TL;DR
If you skip the rest of this post, know this:
- 1Root cause: split-chunk async import + transient feature flag + deploy race → intermittent 404s and SRI mismatch.
- 2Symptom: server returned a tiny 8.42KB shell (was 42.7KB), DOM stopped at 12 nodes, crawlers indexed empty bodies.
- 3Impact: field LCP to 14.3s, INP 7.1s, organic clicks down 0.37% overnight.
- 4Fixes: render critical article content in initial HTML, fail closed with server-side fallbacks, verify CDN chunk availability and SRI before release, and monitor field LCP and HTML body size.
Technical Deep Dive
Failure Mechanism
We introduced a split-chunk async import in the client entry that depended on a transient runtime feature flag. The server rendered only the minimum shell HTML at 8.42KB and referenced a chunk that intermittently 404ed under concurrency.
Vite-generated SRI hashes drifted after a deploy pipeline race, so the browser refused the asset with no useful signal in APM. Lighthouse lab runs hit a warmed cache with the chunk present, so the score stayed inflated while field traffic absorbed the failure.
CrUX showed the damage, but aggregation hid the sharp edge of the incident. Meanwhile, the headless crawler fetched the same shell, timed out on the hydration promise, and indexed what was effectively an empty body - reducing crawl value on affected URLs and contributing to the 0.37% drop in organic clicks.
Reproduction Vector
Deploy pipeline uploads index.html
Referencing chunk/foo.[sha].js with SRI hash.
Concurrent deploys produce race
Some origin nodes get updated SRI and new chunk names while others serve the new index.html but still reference an old chunk or a missing chunk.
Client attempts dynamic import
Based on featureFlag; import intermittently 404s or fails SRI check.
App swallows the error
Using .catch(() => {}) and leaves an empty root; server responded 200 with a shell only.
Crawlers and users receive no content
Headless crawlers/indexers and many real users receive no article content until JS resolves - often too late or never.
The Code That Caused the Bug
useEffect(() => {
import(`./widgets/${featureFlag ? 'new' : 'old'}.js`)
.then(mod => mod.init())
.catch(() => {});
}, [featureFlag]);Why This Is Toxic
- Dynamic import + silent .catch hides the failure and prevents error telemetry from surfacing.
- Runtime boolean for chunk path means different clients request different assets; it's brittle during rolling deploys.
- SRI drift makes the browser refuse assets with no clear error sent to your APM, so HTTP 200s look healthy while the page is empty.
Googlebot & User Perspectives
Headless Googlebot Fetch
- HTML shell: 8.42KB
- Inline links present
- One deferred script
- No hydration → DOM stopped at 12 nodes
- 0 bytes of rendered article text
User Perspective (Chrome)
- LCP: 14.3s
- First meaningful paint: 9.8s
- Blank main pane until chunk arrived
- Many users bailed before content loaded
- Headers healthy, body empty
Metric Regressions
HTML Payload
Before
42.7KB
After
8.42KB
Delta
-34.28KB
Bots indexed an empty article and users waited on client JS - contributing to 0.37% drop in organic clicks.
LCP (lab)
Before
1.14s
After
14.3s
Delta
+13.16s
Perceived performance collapsed; abandonment increased and search landing-page engagement dropped measurably.
DOM Node Count
Before
1,420
After
12
Delta
-1,408
Search engines received a blank shell; synthetic tests diverged from field reality and indexable content vanished.
Field Debugging Checklist
Run these checks when you suspect lab/field divergence:
- Compare synthetic Lighthouse lab scores against CrUX and RUM field metrics for the same URL and release window.
- Curl the page and verify Content-Length, body size, and presence of expected article text in the initial HTML.
- Check SRI hashes and chunk availability on the CDN during the deploy window.
- Run a headless render with Puppeteer and record DOM node count plus innerText length for the main article container.
- Audit deploy artifacts and confirm every chunk referenced by index.html exists on the CDN at the exact SHA.
Detection Signals We Saw
Do not ignore these signals:
- Synthetics showed Performance 92 while real-user LCP exceeded 8.1s in CrUX and RUM, masking a rendering fault until traffic slipped.
- Spike in 200 responses with HTML bodies under 9.8KB while the normal article payload was ~42.7KB - shells being served.
- Support complaints about interactivity rose alongside a 0.37% drop in organic click-throughs from top landing pages.
Copyable CLI Checks
Run these immediately when investigating:
curl -sI https://www.example.com/article/123 | grep -E '^Content-Length|^ETag|^Cache-Control'curl -s https://www.example.com/article/123 | wc -cExample output we saw: Content-Length: 8523, but meaningful text length was 0 - so the crawler received a valid 200 with no article body and the page lost search value.
Quick Test: What Do Bots Actually See?
Most people guess. Don't.
Run this test and look at the actual response your site returns to bots.
Fetch your page as Googlebot
Use your terminal:
curl -A "Googlebot" https://yourdomain.comLook for:
- Real visible text (not just
<div id="root">) - Meaningful content in the HTML
- Page size (should not be tiny)
Compare bot vs browser
Now test what a real browser gets:
curl -A "Mozilla/5.0" https://yourdomain.comIf these responses are different, Google is indexing a different page than your users see.
Stop guessing — measure it.
Real example: 253 words vs 13,547
We see this constantly. Here's a real example from production: Googlebot saw 253 words and 2 KB of HTML. A browser saw 13,547 words and 77.5 KB. Same URL — completely different content.

If your HTML doesn't contain the content, Google doesn't either.
Compare Googlebot vs browser on your site → HTTP Debug ToolCheck for common failure signals
We see this all the time in production:
- HTML under ~1KB → usually empty shell
- Visible text under ~200 characters → thin or missing content
- Missing <title> or <h1> → weak or broken page
- Large difference between bot vs browser HTML → rendering issue
Use the DataJelly Visibility Test (Recommended)
You can run this without touching curl. It shows you:
- Raw HTML returned to bots (Googlebot, Bing, GPTBot, etc.)
- Fully rendered browser version
- Side-by-side differences in word count, HTML size, links, and content
What this test tells you (no guessing)
After running this, you'll know:
- Whether your HTML is actually indexable
- Whether bots are seeing partial content
- Whether rendering is breaking in production
This is the difference between "I think SEO is set up" and "I know what Google is indexing."
If you don't understand why this happens, read: Why Google Can't See Your SPA
If this test fails
You have three real options:
SSR
Works if you can keep it stable in production
Prerendering
Breaks with dynamic content and scale
Edge Rendering
Reflects real production output without app changes
If you do nothing, you will not rank consistently. Learn how Edge Rendering works →
This issue doesn't show up in Lighthouse. It shows up in rankings.
Immediate Remediation Steps
What to do in the next 24-72 hours:
Hotfix: Revert or disable
Revert the split-chunk change or disable the runtime feature flag that selects widget variants until the deploy pipeline and CDN are guaranteed consistent.
Fail closed
Change client entry to render fallback text server-side if dynamic import fails, and return a 5xx or a clear error in telemetry if critical assets are missing.
Add server-side gating
Do not return 200 with an HTML body that lacks expected article text. If article HTML length < 18,237 bytes (our rule), return a flagged response and trigger immediate alerting.
Deploy integrity check
Before cutting traffic, verify every chunk referenced by index.html exists on every CDN origin and SRI matches the build artifact.
Fix instrumentation
Un-suppress import failures - log stack/sha and surface to error tracking. Record hydration failures as a high-severity signal.
Long-term Hardening
Server-render critical content
Move critical article HTML into the server-rendered payload. Reserve client JS for progressive enhancement, not for delivering the only copy of content.
Remove silent catches
Remove silent catches around dynamic imports for critical UI. If you must catch, send detailed telemetry and surface an indicator in production.
CI deploy step
Add a CI deploy step: cross-check index.html references vs artifact manifest and reject deploy if any chunk is missing or SHA mismatched.
Field-first monitoring
Alert when RUM LCP or DOM node count deviates drastically from synthetic labs for the same URL/time window. Treat divergence as a first-class alarm.
Automated crawler validation
Run a headless Googlebot render (Puppeteer with userAgent: Googlebot) on sampling of landing pages post-deploy and compare innerText length of main article container to baseline.
How DataJelly Guard Catches It
Guard monitors real page output: HTML size, visible text, DOM structure, rendering failures, and required sections across deploys. It fires immediately on regression - before your traffic drops.
- Tracks HTML size and visible text per URL
- Detects lab vs field divergence
- Flags DOM drops and blank pages
- Catches SRI mismatches and chunk 404s
- Monitors field LCP and INP in real-time
- Alerts before analytics show the drop
Lighthouse won't lie about the presence of problems - it just runs under different odds than your production fleet.
Your monitors are not omniscient; they are probabilistic. If your synthetic is green while CrUX screams, treat that as an emergency, not an anomaly to ignore. Ship server-rendered content as a primary guarantee, instrument hydration, and make divergence between lab and field a first-class signal.
That's how you stop green dashboards from gaslighting you and keep search traffic from bleeding overnight.
- Senior Staff Engineer, DataJelly Guard