Lighthouse's 0-100 Performance score is a WEIGHTED composite, not a simple pass/fail on Core Web Vitals — which is exactly why a page can have a 'good' LCP by the real Web Vitals thresholds and still score a mediocre 60-something overall, if other weighted metrics (Total Blocking Time especially) are dragging it down. This topic demystifies what actually happens during a Lighthouse run and why lab and field data are meant to disagree sometimes, not just describes 'run Lighthouse and get a score.'
lighthouseperformance-auditinglab-datathrottling
Knowledge Check
15 questions · pass at 70%
0/15
mediummcq
1. What does Lighthouse actually do to produce its report?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
LIGHTHOUSE = drives a REAL headless Chrome instance (not static
analysis) → navigates under SIMULATED throttling (default: slower
network + weaker CPU than the actual host machine) → records a
full performance TRACE (same underlying mechanism as DevTools'
Performance panel) → computes ALL metrics (incl. LCP, CLS) FROM
that trace → each metric → its OWN 0-100 sub-score (scoring curve)
→ WEIGHTED composite = final 0-100 Performance score.
★ Score ≠ simple average of Core Web Vitals. Approx weighting:
TBT ~30% (often LARGEST) · LCP ~25% · CLS ~25% · FCP ~10% ·
Speed Index ~10%. TBT = LAB-ONLY metric, NO direct field Web
Vitals equivalent — main-thread blocking time from long JS tasks.
→ 'Good' field LCP + 'good' field CLS does NOT guarantee a high
Lighthouse score — a bad TBT (heavy long-running JS on load)
can drag the composite down substantially on its own.
LAB (Lighthouse, simulated/controlled) vs FIELD (web-vitals, real
users) — COMPLEMENTARY signals, disagreement is INFORMATIVE, not
a sign either measurement is "wrong."
CI GATING: real-browser-driven → genuine run-to-run VARIANCE.
Use median-of-N runs, threshold WITH buffer, gate on DELTA from
baseline (not just absolute score), keep CI runner resourcing
consistent, investigate the METRIC BREAKDOWN on failure (not just
re-run until green).
Poor TBT → fix = break up long JS tasks (code-splitting, defer
non-critical scripts) — NOT an LCP or CLS fix.
The beginner framing: Lighthouse is a tool (built into Chrome DevTools, also runnable via CLI or CI) that loads a page under controlled conditions and produces a report — including that recognizable 0-100 Performance score — telling you specifically what's slow and, often, exactly how to fix it.
What Lighthouse actually does during those ~10-15 seconds
step 1 / 5
lighthouse https://example.com --view
0ms2400ms4800ms7200ms9600ms12000ms
CLI invoked
launches headless Chrome
Lighthouse launches a real, actual headless Chrome instance — it's not a static analyzer reading your source code, it's driving a genuine browser.
The critical mechanical fact: Lighthouse drives a real, actual headless Chrome instance — it is not statically analyzing your source code or bundle. It navigates to the URL, applies simulated throttling (approximating a slower network and a less powerful CPU than whatever machine is actually running the audit) by default, records a full performance trace of the load using the same underlying Chrome tracing infrastructure DevTools' own Performance panel uses, then computes every reported metric — including LCP and CLS, the same Core Web Vitals covered in the prior topic — by analyzing that trace.
The score is a WEIGHTED composite, not a raw average#
Lighthouse Performance score inputs (approximate current weighting): LCP ~25% Total Blocking Time ~30% ← often the LARGEST single weight Cumulative Layout Shift ~25% First Contentful Paint ~10% Speed Index ~10%
This is the single most important practical fact for interpreting a Lighthouse score correctly: it is NOT a simple average of the Core Web Vitals, and it is NOT identical to "did you pass the good/needs-improvement/poor thresholds" from the raw Web Vitals data. Total Blocking Time (TBT) — a LAB-only metric measuring how long the main thread was blocked by long tasks during load, which has no direct field-measured Core Web Vital equivalent — often carries the single largest weight in the composite score. This is exactly why a page can have a genuinely "good" LCP by the real Web Vitals thresholds and still receive a mediocre overall Lighthouse score: heavy, long-running JavaScript during load can tank TBT even while the visual LCP milestone itself lands on time.
A page's measured metrics: LCP: 2200ms → "good" by the confirmed Web Vitals threshold (≤2500ms) CLS: 0.05 → "good" (≤0.1) TBT: 890ms → very poor (typical "good" TBT is well under 200ms)Overall Lighthouse Performance score: 100? 90? or something notably lower?
Solution
Notably lower — likely somewhere in the 50s-60s range, not 90+. Even though BOTH real Core Web Vitals shown here (LCP and CLS) are individually "good," Total Blocking Time's heavy weighting in the composite score means a very poor TBT (890ms, when "good" is typically well under 200ms) drags the overall number down substantially. This is precisely the gap between "our Web Vitals are good" and "our Lighthouse score is good" — they're related but NOT the same claim, and a team optimizing purely toward passing the field-measured Web Vitals thresholds while ignoring TBT specifically can be caught off guard by a Lighthouse score that doesn't reflect that apparent success. The fix for high TBT is almost always breaking up long-running JavaScript tasks during page load (code-splitting, deferring non-critical scripts) — covered in the code-splitting topic — not something LCP or CLS optimization touches at all.
Build a minimal weighted-score calculator — the actual arithmetic behind the composite Lighthouse number:
// Approximate current Lighthouse v10+ weighting (illustrative — exact weights are versioned and documented,// but the WEIGHTED-COMPOSITE mechanism itself, demonstrated here, is the durable concept):const WEIGHTS = { fcp: 0.10, lcp: 0.25, tbt: 0.30, cls: 0.25, speedIndex: 0.10,};// Each individual metric is first converted to a 0-100 sub-score via Lighthouse's// own scoring curve (not a linear threshold check) — simplified here as pre-computed inputs:function weightedPerformanceScore(subScores) { let
The mechanism: each metric first gets its OWN 0-100 sub-score (via Lighthouse's internal scoring curve, not shown here — the details are versioned and change between Lighthouse releases), and the FINAL number is those sub-scores combined by the weights above. TBT's high weight means a bad TBT sub-score has an outsized effect on the final number even when every other metric scores well — confirming exactly the Try It scenario's outcome mathematically.
The full performance TRACE Lighthouse records under the hood is the SAME underlying data DevTools' Performance panel captures manually — covered in Browser DevTools Mastery — meaning a slow TBT flagged by Lighthouse can be investigated further by manually recording and inspecting the equivalent trace yourself, with the exact same long-task data visible. And TBT specifically measures main-thread blocking from long JavaScript tasks — the direct symptom Runtime Profiling & Memory Leaks covers diagnosing and fixing mechanically.
1. Treating the Lighthouse score as identical to "are our Core Web Vitals good"#
"Our LCP and CLS are both 'good' per field data, so our Lighthouse score must be 90+." // ❌
As shown in Try It — the composite score weights TBT heavily, a lab-only metric with no direct field Web Vitals equivalent; good field-measured LCP/CLS doesn't guarantee a high Lighthouse score at all.
2. Running Lighthouse without accounting for simulated throttling in the interpretation#
"It scored 95 when I ran it on my fiber connection with a fast dev laptop" // misreads WHY it scored well
By default, Lighthouse applies simulated throttling regardless of the actual machine running it — a genuinely bad-performing page can still score reasonably if the simulation doesn't stress the specific bottleneck that real slow-network/low-end-device users would hit; conversely a fine page can score poorly if simulated conditions are more aggressive than most real users' actual conditions. Always read the report's specific metric BREAKDOWN, not just the headline number.
3. Optimizing purely to the Lighthouse score number rather than the underlying real-user experience#
"We need a 100 Lighthouse score" // as a goal in itself, disconnected from field data
Lighthouse is a LAB tool measuring one simulated scenario — chasing a perfect lab score while field (real-user) metrics tell a different story means optimizing for the wrong signal; the score should inform investigation, not be the terminal goal itself.
Read the individual metric breakdown, not just the headline 0-100 score — the score alone doesn't tell you WHICH metric is dragging it down or why.
Pay specific attention to Total Blocking Time given its typically heavy weight — a poor TBT despite good visual metrics (LCP, CLS) usually points to heavy main-thread JavaScript work during load.
Cross-reference Lighthouse (lab) with real-user Web Vitals (field) data — treat disagreement between them as informative, not as one being "wrong."
Run Lighthouse in CI on every deploy (not just occasionally, manually) to catch performance regressions the moment they're introduced, the same discipline recommended for other measurable quality gates in this course.
Use Lighthouse's specific audit recommendations as a starting investigation point, not a literal checklist to blindly implement — some suggestions may not apply to your specific architecture or tradeoffs.
Lighthouse's simulated throttling adds real overhead to each run (deliberately, to approximate realistic conditions) — running it in CI on every single commit (versus on merge to main, or nightly) is a real tradeoff between fast feedback and CI resource cost worth making deliberately.
A high TBT sub-score's outsized weight in the composite means, PERFORMANCE-ENGINEERING-WISE, that breaking up long JavaScript tasks during load is often the highest-leverage single optimization for moving the overall number, more so than marginal LCP or CLS tweaks once those are already in the "good" range.
total
=
0
;
for (const [metric, weight] of Object.entries(WEIGHTS)) {
total += subScores[metric] * weight;
}
return Math.round(total);
}
// The Try It scenario: good LCP/CLS sub-scores, but a terrible TBT sub-score: