Hand-drawn bar chart: The score is a weighted sum of five metrics. Total Blocking Time (TBT) (30%); Largest Contentful Paint (LCP) (25%); Cumulative Layout Shift (CLS) (25%); First Contentful Paint (FCP) (10%); Speed Index (SI) (10%).

The Lighthouse performance score explained as a lab instrument

Read the score as a calibrated lab number: version-stamped weights, a throttling contract you choose on purpose, a median-of-N variance protocol, and an LHCI gate that fails a regression.

Most teams read one Lighthouse number and either celebrate or panic. Neither reaction is earned, because the number is a model output, not a fact about your users. This guide is the lighthouse performance score explained as a chain you can operate. We start at the scoring math, move to the throttling contract, control the run-to-run variance, and finish at a CI gate that blocks a regression.

The lighthouse performance score explained: a weighted bundle, not a vibe#

The performance score is deterministic. It is the sum of five metric sub-scores, each multiplied by a fixed weight. Change one sub-score and the composite moves by exactly that sub-score's weight. Nothing about it is a mood or a grade a reviewer assigns.

Because the weights are public, you can work the arithmetic yourself. Drag the sliders below and watch the composite recompute. Each slider is one metric's 0 to 1 sub-score, and the gauge marker slides across the three score bands as the total changes.

Lighthouse score composer: reweigh the five sub-scores and watch the composite move

Drag each metric sub-score. The composite recomputes live.

73/ 100
Needs improvementweighted composite 0.725

TBT has the most points still on the table. It carries 15.0 unearned points at a 30% weight, more than any other metric on the page. Raising a metric that already sits near 1.00 adds almost nothing, so spend the effort where the weight and the gap are both large.

Weighted contribution per metric (current Lighthouse bundle)
MetricWeightSub-scorePoints earnedPoints available
FCP10%0.909.01.0
SI10%0.858.51.5
LCP25%0.6015.010.0
TBT30%0.5015.015.0
CLS25%1.0025.00.0
Total100%7327.0

Weighted composite 0.725, performance score 73 of 100, band Needs improvement (50 to 89). Biggest gain is TBT: 15.0 points still available at its 30 percent weight. Raising a saturated metric adds nothing.

An illustrative teaching model, not a benchmark. The weights are the published current Lighthouse 10 to 13 bundle (FCP 10%, Speed Index 10%, LCP 25%, TBT 30%, CLS 25%); each sub-score maps from a raw metric value through a log-normal curve, which this composer takes as its input rather than recomputing. Real scores depend on your throttling mode and run-to-run variance, so read the median of several runs under a fixed device and network preset before you trust a number.

Drag each metric sub-score from 0 to 1. The island multiplies each by its published weight, sums them, and shows the 0 to 100 score, its band, and the points still available per metric. The contribution table is the accessible source of truth; the gauge is decorative. Every figure is illustrative teaching math against the published weights, never a benchmark of a real page.

Notice what the composer proves. A metric with a small weight can be perfect and still barely move the score. Therefore the score is not "how many audits passed". That is the lighthouse performance score explained as a weighted average, where the weights alone decide where your effort pays off.

The five metrics and their current weights#

Weights drift by major version, so any cited number must name the version. The table below stamps the current Lighthouse 10 to 13 bundle beside the two prior generations. Read down your metric, then across to see how its weight has changed.

Lighthouse performance-score weights by major version (source: developer.chrome.com performance-scoring)
MetricLighthouse 10 to 13 (current)Lighthouse 8 to 9Lighthouse 6 to 7
First Contentful Paint (FCP)10%10%15%
Speed Index (SI)10%10%15%
Largest Contentful Paint (LCP)25%25%25%
Total Blocking Time (TBT)30%30%25%
Cumulative Layout Shift (CLS)25%15%5%
Time to Interactive (TTI)removed10%15%

Two shifts matter most. First, Lighthouse 10 dropped Time to Interactive and moved its weight into CLS, which jumped from 15 to 25 percent. Second, TBT has carried the single heaviest weight since version 8. So a stale blog citing v6 weights will send you to optimize the wrong metric.

How a raw metric becomes a 0-1 sub-score#

Each raw value passes through a log-normal curve before it becomes a sub-score. The curve is calibrated against real-site data, so the mapping is steep in the middle and flat at the ends. That shape is why small raw improvements near the top of the range barely move the score.

The composer above takes each sub-score as its input, rather than recomputing the curve. That is deliberate. In practice you read the sub-scores straight off the report, then reason about the weighted sum, which is where the largest score gains live. For the exact weights and curve, the Chrome performance-scoring reference is the primary source, and the official scoring calculator lets you replay any set of metrics.

Work the composite by hand#

A worked example is the lighthouse performance score explained in numbers. Take a real set of sub-scores: FCP 0.90, Speed Index 0.85, LCP 0.60, TBT 0.50, and CLS 1.00. The weighted sum is (0.90 x 0.10) + (0.85 x 0.10) + (0.60 x 0.25) + (0.50 x 0.30) + (1.00 x 0.25). That is 0.090 + 0.085 + 0.150 + 0.150 + 0.250, which equals 0.725. So the performance score is 73, which lands orange.

Now spend a fixed unit of effort and see where it pays. CLS already sits at 1.00, so layout-shift work adds zero points. TBT carries the 30 percent weight instead. Raising TBT's sub-score from 0.50 to 0.90 adds (0.90 minus 0.50) x 0.30, which is 0.12, lifting the score to 85. The same 0.40 sub-score gain on FCP, at a 10 percent weight, adds only 0.04.

Same 0.40 sub-score gain, very different score movement3x the movement per unit of effort
Heaviest weight

+12 points

Fix TBT (30% weight): 0.50 to 0.90

+4 points

Fix FCP (10% weight): 0.50 to 0.90

Identical raw improvement, three times the score gain, purely because TBT carries three times the weight of FCP. The weight bundle, not the effort, decides the payoff. Read the weights before you pick a target.

Same 0.40 sub-score gain, very different score movement (points added to the performance score)
Optionpoints added to the performance score
Fix TBT (30% weight): 0.50 to 0.90+12 points
Fix FCP (10% weight): 0.50 to 0.90+4 points

Source: developer.chrome.com performance-scoring

This is the whole argument against chasing vanity points. Moreover the composer's biggest-gain line makes the same call live, by naming the metric with the most unearned points at the highest weight.

Reading Opportunities vs Diagnostics without chasing vanity points#

The report splits findings into Opportunities and Diagnostics. Neither section is scored directly. Each item only matters through the metric it moves, and each metric only matters through its weight.

So the reading order is metric first, audit second. Because the audit list is sorted by estimated savings, not by weighted impact, it will happily send you to gold-plate a metric that is already green.

"Mobile" is a throttling contract, not a viewport#

A mobile Lighthouse run is not just a narrow screen. It is a deliberate device and network throttling contract. Understanding lighthouse simulated throttling vs applied throttling is what makes two runs of the same page comparable, or not.

Simulated vs applied vs no throttling#

There are three throttling modes, and they produce different numbers. Simulated throttling estimates a throttled load from an unthrottled one via the Lantern model. Applied throttling actually slows each request during the load. No throttling runs on your real connection.

The three Lighthouse throttling modes and which tool defaults to each
ModeMechanismSpeedFidelity to a real deviceVarianceDefault in
Simulated (Lantern)Estimates a throttled load from an unthrottled one via a modelFastestApproximateLowerPageSpeed Insights, Lighthouse CLI, LHCI
Applied (DevTools)Slows each request in real time during the loadSlowerHigherHigherDevTools Lighthouse panel
No throttlingRuns on your machine and connection as-isFastOnly your own machineHighestOpt-in only, no tool defaults to it

The trap is comparing across modes. A PageSpeed Insights score uses simulated throttling, while a DevTools score uses applied throttling, so they can disagree by double digits on the same page. Therefore pick one mode for your gate and never mix them run to run. The Lighthouse throttling documentation spells out how each mode is applied and where the numbers diverge.

Why the same page scores lower on mobile#

The lighthouse mobile vs desktop score difference is not a bug. It is two different device presets. The mobile preset simulates a mid-tier phone on a slow network, and the desktop preset does not.

This is also where backend latency shows up. A slow first byte inflates LCP under either preset, so backend bottlenecks that tank your Lighthouse score before the frontend even loads deserve a look before any image trick. In contrast, the CPU multiplier mostly punishes heavy JavaScript, which lands on TBT.

Getting a number you can gate on#

Ask "why is my lighthouse score different every time" and the answer is variance. A single run is one sample from a noisy distribution. Consequently you cannot gate on it without flaky failures.

Why your score changes every run#

Run the same page five times on the same machine and the scores spread. The spread comes from CPU contention, network jitter, and the modeling itself. So a single run can land anywhere in the band, and picking one run to gate on invites noise.

Show data table
Five back-to-back runs of one unchanged page, same machine and preset (illustrative)
Item Performance score
Run 1 82 pts
Run 2 88 pts
Run 3 84 pts
Run 4 79 pts
Run 5 86 pts

Nothing about the page changed, yet the five runs span 79 to 88, a nine-point spread. The median is 84 (Run 3). Gate on one run and you would flake; gate on the median of five and the number holds steady. These are illustrative figures, not a benchmark of a real page.

Figure Five back-to-back runs of one unchanged page, same machine and preset (illustrative) Five back-to-back runs of one unchanged page on one machine at one preset. Modelled, not measured.

Sample size is the lesson here. Because run-to-run variance is real, a stable gate needs several runs and a robust aggregate. A median of an odd sample is the honest choice, since one outlier run cannot drag it. The Lighthouse variability guide documents the sources of noise and why a single run misleads.

The median-of-N protocol#

The fix is a median of several runs under fixed conditions. A median of 3 lighthouse runs already removes most single-run noise, and five is steadier still. Run N times, then aggregate by median before you compare.

median-of-n.sh · bash
# Collect five runs, then assert against the MEDIAN. The median of an
# odd sample stays stable against a single slow or fast outlier run,
# which a mean would let drag the number around.
npx @lhci/cli collect \
  --url=https://example.com/ \
  --numberOfRuns=5 \
  --settings.preset=desktop        # pick the throttling contract on purpose

# assert reads .lighthouseci/ and aggregates by median-run BEFORE comparing,
# so the gate judges the stable middle, not whichever run finished last.
npx @lhci/cli assert --preset=lighthouse:recommended

Notice the two steps. First, collect fires N runs and stores each result. Second, assert reads the stored runs and aggregates by median before judging. So the gate sees one stable number, not whichever run finished last.

The controlled-environment checklist#

Because these hold the throttling contract steady, the median stops wandering. As a result you get a number stable enough to gate on, and any real regression stands out against it.

Gating a PR on a regression with LHCI#

Now the payoff. A stable median is only useful if it blocks a regression before it merges. This is where Lighthouse CI (LHCI) turns the lab number into a gate. Think of this section as the lighthouse ci (LHCI) tutorial the setup docs stop short of.

The gate pipeline, from pull request to pass or fail#

The pipeline is a straight line with one branch. A pull request triggers a collect of N runs, the runs aggregate by median, and the median asserts against a preset plus your custom rules. Then severity decides the outcome.

The LHCI gate pipeline from a pull request through median aggregation to a pass or fail checkA pull request triggers LHCI to collect N runs, aggregate them by median, and assert against a preset plus custom rules. A warn-severity assertion logs but still passes; an error-severity assertion fails the status check and blocks the merge until the author fixes the regression and pushes again.

Read the branch as the whole point. Because a warn logs without blocking and an error fails the check, you choose per rule how strict to be. So a metric you are still tuning warns, and a metric you have earned errors.

A complete lighthouserc.js, budgets.json, and GitHub Action#

Here is a working lighthouse ci github actions setup in three parts. Switch tabs to compare the config, the budgets, and the workflow. Each is annotated so you can see LHCI assertions and performance budgets wired together.

lighthouserc.js · js
// lighthouserc.js — collect N runs, aggregate by median, then assert.
module.exports = {
  ci: {
    collect: {
      url: ['https://example.com/'],
      numberOfRuns: 5,                    // sample size, never a single run
      settings: {
        preset: 'desktop',                // the throttling contract, chosen
        budgetsPath: './budgets.json',    // wire the resource budgets in
      },
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        // error blocks the merge; median-run judges the stable middle.
        'categories:performance': [
          'error',
          { minScore: 0.9, aggregationMethod: 'median-run' },
        ],
        // warn logs but does not block, for a metric you are still tuning.
        'total-blocking-time': [
          'warn',
          { maxNumericValue: 200, aggregationMethod: 'median-run' },
        ],
        'largest-contentful-paint': [
          'error',
          { maxNumericValue: 2500, aggregationMethod: 'median-run' },
        ],
      },
    },
    upload: { target: 'temporary-public-storage' },
  },
};

Read the config as a contract. The numberOfRuns sets the sample size, the aggregationMethod pins the median, and each assertion picks warn or error. Because budgetsPath wires the budgets in, the same run checks both the score and the byte weight. The LHCI configuration reference lists every assertion knob, and the @lhci/cli package ships the collect and assert commands.

Assertions vs budgets, warn vs error#

Assertions and budgets catch different regressions. An assertion judges an audit score or a numeric metric, while a budget judges resource sizes and counts. Use both, since a bundle can bloat while the rounded score still reads green.

LHCI assertions versus budgets.json, and how each blocks a merge
AspectAssertions (lighthouserc)budgets.json
What it checksAudit scores and numeric metric valuesResource sizes and counts
Key knobminScore, maxNumericValueresourceSizes, resourceCounts
Severitywarn or error, per assertionwarn or error, per budget
Stabilized byaggregationMethod: median-run over NEvaluated per run, so still needs median-run
Blocks a merge whenAn error-severity assertion failsAn error-severity budget is exceeded

The knob that stops flaky gates is aggregationMethod. Because median-run judges the stable middle of N runs, a single slow run cannot fail the check. So set the sample size high enough that the median is quiet, then let error severity do the blocking.

Lighthouse is a lab signal, so hand prioritization to the field#

Here is the boundary that keeps this whole method honest. Lighthouse lab data vs CrUX field data answer two different questions, and neither replaces the other. The lab gives you a repeatable regression signal, and the field gives you a real-user verdict.

The lab signal versus the field verdict, with a handoff from gating to prioritizationTwo lanes. The lab lane (Lighthouse) runs a fixed device and network contract, so it produces a repeatable signal you gate a pull request on. The field lane (CrUX or your own RUM) reads real users at the 75th percentile, so it tells you which page actually hurts users and which fix to prioritize. Gate regressions on the lab number here; prioritize fixes from the field there.

So use each lane for its job. Gate a pull request on the lab median, because it is repeatable and catches a change before it ships. Then prioritize what to fix from the field, because only real-user p75 data knows which page hurts. For the full prioritization workflow, read the full site-optimization playbook for speed, performance, and UX, which owns the field-first side of this handoff. For the delivery side of first-byte timing, how rendering strategy on Cloudflare affects real-user load timing attacks the LCP that both lanes read.

When NOT to gate on Lighthouse#

There is one more limit worth naming. A green score on a page nobody visits is worth nothing, and a red score on your checkout is worth everything. So the lab number tells you a page got slower, not whether that page matters. In short, the lighthouse performance score explained honestly is a regression alarm, not a priority list.

Talk to us about a performance audit and CI gate

Keep reading