The Lighthouse score
How Lighthouse turns five metric values into a score from 0 to 100, why it moves between runs, and what it can't tell you. Lighthouse 13.6.0's scoring, ported and checked against 30 recorded runs.
- 01Work out a scoreRead a metric's value off its curve to get its score, then weigh the five scores into the Performance score.
- 02Read the wobbleSay why the score moves between runs when nothing changed, and compare the medians of several runs instead of single runs.
- 03Know what it leaves outExplain why INP isn't in the score, and why a page that scores 100 can still feel slow to its users.
One number from five metrics
You run Lighthouse on the Leaf and Kettle store, the slow store from How Lighthouse works. At the top of the report sits a circle with a number in it: 90, in green. You run it again without changing a single line. This time the circle says 89, in orange.
Did the store get slower in the last minute? The course saw exactly this. It ran Lighthouse 13.6.0 on the store ten times in a row, changing nothing, and got seven 90s and three 89s. Here is the number from each of the ten reports:
Nothing changed between the runs. Still, the number moved by one point, and the colour flipped with it. So three questions. Where does the number come from? Why does it move? And if the store scored 100, would it feel fast on Asha's phone?
The number is the Performance score: Lighthouse's summary of one page load, from 0 to 100. Its colour says which of three bands it fell in. Chrome's docs set the bands:
- 90 to 100, green: good.
- 50 to 89, orange: needs improvement.
- 0 to 49, red: poor.
So the store sits right on the line between green and orange. Keep that in mind for later.
Five measurements go in
Lighthouse doesn't score the page as a whole. While the page loads, it measures five things, called metrics. Here is what it measured on the store's first run:
// What Lighthouse measured on the store's first run.const run = { FCP: 1943, SI: 2337, LCP: 2693, TBT: 44.5, CLS: 0.136 }; console.log(`The first text or image appeared at ${run.FCP} ms.`);console.log(`The page filled in with a Speed Index of ${run.SI} ms.`);console.log(`The biggest picture or block of text appeared at ${run.LCP} ms.`);console.log(`Long tasks blocked the main thread for ${run.TBT} ms.`);console.log(`Layout shifts added up to a score of ${run.CLS}.`);Line 2 holds the five values, under the short names the report uses. Lines 4 to 8 print one sentence for each. Here is what each metric measures:
- FCP, First Contentful Paint: when the first text or image appeared. Line 4 prints
The first text or image appeared at 1943 ms. - SI, Speed Index: how quickly the page filled in. Lighthouse films the load and measures how fast the screen got close to its final look. Line 5 prints
The page filled in with a Speed Index of 2337 ms. - LCP, Largest Contentful Paint: when the biggest picture or block of text appeared. Line 6 prints
The biggest picture or block of text appeared at 2693 ms. - TBT, Total Blocking Time: how long the main thread was too busy to answer a click. The main thread is the one that runs the page's JavaScript. Line 7 prints
Long tasks blocked the main thread for 44.5 ms. - CLS, Cumulative Layout Shift: how much the content jumped around. It has no unit: 0 means nothing moved. Line 8 prints
Layout shifts added up to a score of 0.136.
Four are times in milliseconds (ms), thousandths of a second. CLS is a plain number. So how do five different numbers become one score?
Three steps to one number
It takes three steps:
- Measure. Load the page once and record the five values.
- Score each value. Read each value off its own curve. That gives a score from 0 to 1, where 1 is as good as it gets.
- Weigh and add. Multiply each score by its metric's weight, how much that metric counts. Then add them up. The weights add up to 100, so the total is out of 100.
Here are the three steps on the store's first run, all at once:
Read it from left to right. LCP's 2,693 ms became a score of 0.85. TBT's 44.5 ms became a perfect 1. Each score was multiplied by its weight, and the points came to 89.65. The report rounds that to 90.
At the end of term, your school report gives each subject a grade. The overall mark at the bottom isn't a plain average: the school counts some subjects more than others. So a weak grade in a subject that counts a lot pulls the overall mark down further than the same grade in one that counts a little.
- In real life: Your mark in each subject's test
- In Lighthouse: Each metric's value, like an LCP of 2,693 ms
- In real life: The grade each subject gets
- In Lighthouse: Each metric's score, from 0 to 1
- In real life: Maths counting more than art
- In Lighthouse: The weights: TBT counts 30, FCP only 10
- In real life: The overall mark at the bottom
- In Lighthouse: The Performance score
The Performance score is a weighted average of five metric scores. Each score comes from reading a metric's value off a curve. It sums up one test load in the lab, not what your users go through.
Try it before reading on. The playground starts at the store's first run. Move any value, or type one, and watch the score change. Then switch to the desktop curves: most of the same values score lower there.
Each value gets a score from Lighthouse's own mobile curve. Then the weights add the five scores up. CLS loses the most: 5 of its 25 points.
The next two sections take the steps apart, weights first. The three after them answer the opening questions.
Metric weights
Start with step three, the adding up. The store kept 89.65 points out of 100. Where did the other 10.35 go?
A weight is how much a metric counts toward the score. These are the weights in Lighthouse's default settings, read from the course's own runs:
- Total Blocking Time (TBT): 30
- Largest Contentful Paint (LCP): 25
- Cumulative Layout Shift (CLS): 25
- First Contentful Paint (FCP): 10
- Speed Index (SI): 10
They add up to 100. So each weight is also the most points that metric can give. A perfect TBT is worth 30 points. A perfect FCP is worth only 10. That's the school report's "some subjects count more".
Why these numbers? Chrome's docs say "The weightings are chosen to provide a balanced representation of the user's perception of performance." They add that "The weightings have changed over time", as the Lighthouse team learns more. So the same page can score differently in two versions of Lighthouse.
Only the five metrics count
A report holds many more checks than five. Lighthouse calls each check an audit. Some audits are insights: they point at likely causes, such as a file that holds up the first paint. In the course's runs, every audit except the five metrics had a weight of 0, insights included. Chrome's docs say it plainly: "only metrics contribute to your Lighthouse Performance score".
Insights still matter. The same docs say fixing them can "improve the metric values, so there is an indirect relationship." They're like a teacher's comments in the margin. The comments don't change your grade, but following them can raise the next one. So fix an insight, run Lighthouse again, and the score moves only if a metric moved.
Adding up the store's points
Here is step three as code, on the store's first run:
// The store's first run: each metric's score, from 0 to 1.const scores = { FCP: 0.86, SI: 0.98, LCP: 0.85, TBT: 1, CLS: 0.8 };// How much each metric counts. The weights add up to 100.const weights = { FCP: 10, SI: 10, LCP: 25, TBT: 30, CLS: 25 }; let points = 0;for (const metric in weights) points += scores[metric] * weights[metric];console.log(`The store kept ${points.toFixed(2)} of 100 points.`);console.log(`The report shows ${Math.round(points)}.`);- Line 2 holds the five metric scores, from the run's saved results. The report page doesn't show them. Line 4 holds the weights.
- Line 6 starts the total at 0. Line 7 goes through the metrics one by one and adds score times weight. For LCP, that's 0.85 times 25, which is 21.25.
- Line 8 prints
The store kept 89.65 of 100 points. - Line 9 rounds to a whole number, as the report does. It prints
The report shows 90.
Now turn it around. Each metric had its weight to give, and kept only part of it. What it lost is its weight times the gap between its score and 1:
// The store's first run again: its scores, and the weights.const scores = { FCP: 0.86, SI: 0.98, LCP: 0.85, TBT: 1, CLS: 0.8 };const weights = { FCP: 10, SI: 10, LCP: 25, TBT: 30, CLS: 25 }; for (const metric in weights) { const lost = (1 - scores[metric]) * weights[metric]; console.log(`${metric} lost ${lost.toFixed(1)} points.`);}- Lines 2 and 3 are the same scores and weights.
- Line 6 works out each metric's loss. CLS scored 0.8, so it lost 0.2 of its 25 points.
- Line 7 prints one line per metric:
FCP lost 1.4 points.,SI lost 0.2 points.,LCP lost 3.8 points.,TBT lost 0.0 points.andCLS lost 5.0 points.
The layout shift cost the most. The store's content jumped by 0.136, which scored 0.8, and CLS weighs 25. That cost 5 points. TBT, the heaviest metric, cost nothing: its 44.5 ms scored a perfect 1. So to raise this store's score, start with the jump, not with the JavaScript. Look at the points each metric lost, not at which value looks worst.
Log-normal scoring curves
Now step two. The store's LCP was 2,693 ms, and it scored 0.85. Why 0.85, and not 0.7 or 0.95? Who decides what a good LCP is worth?
Some teachers grade on a curve. They don't fix the pass mark before the test. They look at how the whole class did, and set the grades from that. Your grade says how you did compared with everyone else who sat the same test.
- In real life: The whole class's marks
- In Lighthouse: Real sites' metric values, from HTTP Archive
- In real life: The mark where the top grade starts
- In Lighthouse: The value that scores 0.9, called
p10 - In real life: The mark where a pass starts
- In Lighthouse: The value that scores 0.5, called
median - In real life: Your grade
- In Lighthouse: Your metric's score, read off the curve
Lighthouse grades each metric against real websites. They come from the HTTP Archive, a public project that loads millions of web addresses every month, on desktop and on mobile. Its FAQ says it runs Lighthouse on each of them too.
From that data, Lighthouse picks two values for each metric, called control points. Chrome's docs give the rule in percentiles. The 25th percentile is the value that a quarter of the sites matched or beat. The 8th percentile is the value that only 8 sites in 100 matched or beat.
- The value at the 25th percentile scores 0.5. Lighthouse calls it the
mediancontrol point. It's where orange starts. - The value at the 8th percentile scores 0.9. Lighthouse calls it
p10. It's where green starts.
Each metric's own file can say more. For mobile LCP, the points are 4,000 ms and 2,500 ms. The audit's source says they come from the "25th and 13th percentiles" of HTTP Archive data. The same comment links to web.dev's page on a good LCP, which also draws its line at 2.5 seconds.
The curve has a shape called log-normal. Statistics uses it for values that can't go below zero and have a long tail of big ones, like load times. Chrome's docs call Lighthouse's curve "a log-normal distribution derived from the performance metrics of real website performance data on HTTP Archive." The two control points fix where the curve sits and how steep it is.
The curve in Lighthouse's own code
Here is Lighthouse's own code for it, shortened. You don't need to follow the maths in the first function. Read it as "the curve's shape".
// Lighthouse's curve, shortened from its source (shared/statistics.js and shared/util.js).function erf(x) { const sign = Math.sign(x); x = Math.abs(x); const t = 1 / (1 + 0.3275911 * x); const y = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429)))); return sign * (1 - y * Math.exp(-x * x));} function score({ p10, median }, value) { const x = (Math.log(value / median) * 0.9061938024368232) / -Math.log(p10 / median); let share = (1 - erf(x)) / 2; if (value <= p10) share = Math.max(share, 0.9); if (share > 0.9) share += 0.05 * (share - 0.9); return Math.floor(share * 100) / 100;}- Lines 2 to 8 are
erf(), the error function, a standard function from statistics. JavaScript doesn't have one, so Lighthouse writes out a well-known formula that comes very close to it. - Line 10 starts
score(). It takes a metric's two control points and a value. - Line 11 compares the value with the median. It doesn't look at the gap in milliseconds. It looks at the ratio: how many times bigger or smaller the value is. That's the "log" in log-normal.
- Line 12 turns that into a share between 0 and 1. Lighthouse's source says the score "represents the amount of the distribution greater than" the value. In other words: how much of the curve sits on the slower side of you.
- Line 13 makes sure a value at or below
p10scores at least 0.9. Lighthouse has two more checks like it, one for each other colour. They're left out here. - Line 14 gives scores above 0.9 a small boost, "so more top scores will be a perfect 1", as the source puts it.
- Line 15 cuts the score down to two decimals. It always rounds down, so 0.859 becomes 0.85.
Now use it. The next block reads three LCPs off the mobile curve:
// score() is Lighthouse's curve, from the block above.const lcp = { p10: 2500, median: 4000 }; console.log(`An LCP of 2500 ms scores ${score(lcp, 2500)}.`);console.log(`An LCP of 4000 ms scores ${score(lcp, 4000)}.`);console.log(`The store's LCP, 2693 ms, scores ${score(lcp, 2693)}.`);- Line 1 says where
score()comes from. Line 2 holds the mobile LCP curve's two control points. - Lines 4 and 5 try the control points themselves. They print
An LCP of 2500 ms scores 0.9.andAn LCP of 4000 ms scores 0.5. - Line 6 tries the store's LCP. It prints
The store's LCP, 2693 ms, scores 0.85.
That's the store's 0.85. Its LCP was a little slower than the start of green, and a long way faster than the start of orange. Here is the whole curve. The other dot is the fast store, which you'll meet at the end of the lesson:
Steep in the middle, flat at the top
Look at the curve's shape. In the middle, it's steep: a small change in LCP moves the score a lot. Near the top, it flattens out: the same change hardly moves it at all.
On sports day, a chart turns your long jump into points. If you jump an average distance, 10 cm more lifts your points a lot. If you already jump further than nearly everyone, the same 10 cm adds almost nothing, because the chart squeezes the top together. The last few points are the hardest to earn.
- In real life: Jumping 10 cm further
- In Lighthouse: Making LCP 500 ms faster
- In real life: An average jumper gaining a lot of points
- In Lighthouse: A score in the steep middle of the curve rising a lot
- In real life: A top jumper gaining almost nothing
- In Lighthouse: A score near 1 hardly moving
- In real life: The points chart
- In Lighthouse: The metric's curve
// score() is Lighthouse's curve, from the block above.const lcp = { p10: 2500, median: 4000 }; // Two pages each make their LCP 500 ms faster.console.log(`From 4500 ms to 4000 ms: ${score(lcp, 4500)} to ${score(lcp, 4000)}`);console.log(`From 2000 ms to 1500 ms: ${score(lcp, 2000)} to ${score(lcp, 1500)}`);- Line 2 is the same mobile LCP curve. Line 4 sets the scene: two pages each save 500 ms.
- Line 5 is a page in the middle of the curve. It prints
From 4500 ms to 4000 ms: 0.37 to 0.5 - Line 6 is a page near the top. It prints
From 2000 ms to 1500 ms: 0.97 to 1
The same 500 ms bought 0.13 in the middle and 0.03 near the top. LCP weighs 25, so that's about 3 points of the Performance score against less than 1. Chrome's docs describe the same shape. Between scores of 0.5 and 0.92, the value and the score move almost in step. Around 0.96 is "the 'point of diminishing returns'": above it, each extra point takes more work.
The docs also put a number on it: "taking a score from 99 to 100 needs about the same amount of metric improvement that would take a 90 to 94." On the mobile LCP curve, 90 to 94 takes LCP from 2,500 ms to 2,274.8 ms. 99 to 100 takes it from 1,792 ms to 1,545.7 ms. Each takes roughly a quarter of a second off LCP, for 4 points in one place and 1 in the other. Any LCP up to 1,545.7 ms scores 1, so going faster than that gains nothing more.
Why it matters: when you choose what to fix, a metric in the steep part of its curve moves the score the most.
Desktop runs use their own curves, with stricter control points. For LCP, they're 1,200 ms and 2,400 ms. Chrome's docs explain why. Before Lighthouse 6, desktop runs used the mobile curves, and that "led to artificially inflated desktop scores."
Why INP isn't in the score
Asha had a second complaint about the store. She tapped Add to cart, and for a moment nothing happened. That delay has its own metric: Interaction to Next Paint, or INP. It measures how long a page takes to show a response after a click, a tap or a key press. web.dev calls it "a stable Core Web Vital metric".
Yet INP isn't one of the five metrics. Why would the score leave out a Core Web Vital?
You want to know how long Leaf and Kettle takes to serve a cup of tea. So you visit at seven in the morning, before it opens. Nobody orders, so there's no order to time. But you can watch the cook. If the cook spends twenty minutes on one long job, anyone who ordered then would have waited.
- In real life: Visiting before the doors open
- In Lighthouse: A navigation: one page load, with no clicks
- In real life: No order to time
- In Lighthouse: No interaction, so no INP
- In real life: The cook stuck on one long job
- In Lighthouse: A long task keeping the main thread busy
- In real life: Adding up how long the cook was stuck
- In Lighthouse: Total Blocking Time
A Lighthouse navigation, its default mode, is that early visit. It loads the page and measures the load. Nobody clicks anything. Lighthouse's INP audit lists only one mode it works in, timespan, so a navigation doesn't run it at all. The course checked: the store's navigation reports had no INP audit, and no INP in their Performance category.
INP in a timespan
A timespan is Lighthouse's mode for a stretch of time you choose, such as a click. The course recorded one on the fast store, which you'll meet in the last section. It started a timespan, clicked Add to cart, and stopped two seconds later. The click took 302 ms. INP is in a timespan's Performance category, but with a weight of 0. So the slow click counted for nothing.
A timespan also imitates the slow phone in a different way. By default, Lighthouse loads the page at full speed, then works out how long a slow phone would have taken. That way is called simulate, and the model doing the sums is called Lantern. Throttling and simulation takes it apart.
Lantern can't work out responses to clicks yet. The INP audit's source says so: "responsiveness isn't yet supported by lantern". So for a timespan, Lighthouse's config switches to devtools throttling, which slows the page down while it really loads.
TBT stands in for INP
So in a page load, Lighthouse watches the cook instead. A long task is a piece of work that keeps the main thread busy for more than 50 ms. web.dev defines a long task's blocking time as "its duration in excess of 50 milliseconds". Total Blocking Time adds those up for every long task after the first text or image appears. A click that lands during a long task has to wait for it. So a high TBT warns of a slow INP.
web.dev says "TBT predates INP and is useful as an indicator of INP issues, particularly in the lab environment where measuring INP is more difficult." It also warns that TBT is "not a substitute for INP in and of itself." TBT is also the heaviest metric in the score, at 30.
The two are easy to mix up. Read this table across: each row asks one question, answered for TBT and for INP.
| Question | Total Blocking Time (TBT) | Interaction to Next Paint (INP) |
|---|---|---|
| When is it measured? | During a page load, in the lab. | When someone clicks, taps or presses a key. |
| What does it add up? | The part of each long task over 50 ms, after the first text or image appears. | How long the page took to respond. Over a whole visit, it's the slowest response, leaving out rare outliers. |
| Does it count in a navigation's score? | Yes. It weighs 30, more than any other metric. | No. A navigation doesn't measure it at all. |
| Is it a Core Web Vital? | No. web.dev calls it a lab metric: a warning sign for slow responses. | Yes. It's the Core Web Vital for responsiveness. |
For real users, INP comes from Event Timing entries, and long tasks from Long Tasks entries. These boxes show which browsers report them:
Why the score moves between runs
Back to the opening question. The store scored 90, then 89, with nothing changed. What moved?
You step on a bathroom scale: 60.2 kg. You step off and on again: 60.4 kg. You didn't gain 200 grams in a minute. The way you stood, the floor, and the scale itself all wobble a little. To know whether you really changed, you weigh yourself a few times and take the middle reading.
- In real life: Stepping on the scale
- In Lighthouse: Running Lighthouse once
- In real life: 60.2 kg, then 60.4 kg, a minute apart
- In Lighthouse: 90, then 89, with nothing changed
- In real life: How you stand, the floor, the scale itself
- In Lighthouse: The machine, the network, the browser
- In real life: Weighing yourself five times and taking the middle
- In Lighthouse: Running Lighthouse five times and comparing medians
Lighthouse wobbles the same way. Its docs say scores "will change due to inherent variability in web and network technologies, even if there hasn't been a code change." Here is the store's first run next to its sixth:
// The store's first run and its sixth run. Nothing changed in between.const weights = { FCP: 10, SI: 10, LCP: 25, TBT: 30, CLS: 25 };const first = { FCP: 0.86, SI: 0.98, LCP: 0.85, TBT: 1, CLS: 0.8 };const sixth = { FCP: 0.86, SI: 0.98, LCP: 0.82, TBT: 0.99, CLS: 0.8 }; for (const [name, scores] of Object.entries({ first, sixth })) { let points = 0; for (const metric in weights) points += scores[metric] * weights[metric]; console.log(`The ${name} run kept ${points.toFixed(2)} points and shows ${Math.round(points)}.`);}- Line 2 holds the weights. Lines 3 and 4 hold the two runs' metric scores. Only LCP and TBT differ.
- Lines 6 to 10 add up each run's points, the same way as the weights example.
- Line 9 prints
The first run kept 89.65 points and shows 90.andThe sixth run kept 88.60 points and shows 89.
The sixth run's LCP came at 2,834 ms, 141 ms later than the first run's 2,693 ms. On the curve, that's 0.82 instead of 0.85: three quarters of a point. Its TBT was 81 ms instead of 44.5 ms, which cost another 0.3. That's about one point in all.
One point doesn't matter on most pages. But the store sits right next to 89.5, the line between showing 89 and showing 90, and between orange and green. So a one-point wobble flips both the number and the colour. It's the scale reading 59.9 one minute and 60.1 the next.
Where the wobble comes from
Chrome's docs say "A lot of the variability in your overall Performance score and metric values is not due to Lighthouse." They list common causes:
- A/B tests, or ads that change from one load to the next.
- Internet traffic taking a different route.
- Testing on different devices, such as a fast desktop and a slow laptop.
- Browser extensions that add JavaScript or change requests.
- Antivirus software.
Lighthouse's variability docs add a warning about the machine itself: "DO NOT collect multiple Lighthouse reports at the same time on the same machine." Runs at the same time compete for the same processor and memory.
To see how much a busy computer matters, the course ran the store twenty times, taking turns. Ten runs had a quiet machine. Ten ran while 8 threads, one per processor core, kept every core busy:
// The store's Performance scores: ten runs on a quiet machine, ten with every core busy, taking turns.const runs = { "a quiet machine": [90, 90, 90, 90, 90, 89, 89, 90, 89, 90], "a busy machine": [89, 89, 90, 89, 88, 90, 90, 90, 89, 89],}; for (const [machine, scores] of Object.entries(runs)) { const sorted = [...scores].sort((a, b) => a - b); console.log(`On ${machine}: median ${sorted[Math.floor(sorted.length / 2)]}, from ${sorted[0]} to ${sorted.at(-1)}.`);}- Lines 3 and 4 hold the twenty recorded scores.
- Line 8 sorts each list. Line 9 prints its middle value, the median, then its lowest and highest.
- It prints
On a quiet machine: median 90, from 89 to 90.andOn a busy machine: median 89, from 88 to 90.
The busy machine pulled the median down by one point and widened the range. The playground holds every run. Pick a set to see each run's score and values:
The store, on a quiet machine. Nothing changed between runs. Over 10 runs: median 90, 89 to 90. Every 89 had an LCP of 2,834 ms or more. Every 90 had 2,693 ms or less.
Why did the store move so little, even on a busy machine? It was served from the same computer, so no real network was involved. And the docs say Lighthouse's default simulate throttling gives "both quick evaluation and minimized variance". It even "partially mitigates" a busy machine. Over a real network, the docs list more causes, such as the local network and the web server.
The report warns you itself. Under the score, it says: "Values are estimated and may vary." So never trust one run. The variability docs say "The median Lighthouse score of 5 runs is twice as stable as 1 run." Run each version of your page five times, take the medians, and compare those. That's stepping on the scale five times.
Why a 100 can still be slow
Last question: what would a 100 mean? To find out, the course built a fast store. It started from the course's first slow store, from Your toolkit and the course project, and fixed four things:
- Its script is deferred, so it no longer holds up the first paint.
- Its picture is the light one, not the 580 KB one.
- It no longer sorts 200,000 prices while the page starts.
- It keeps space for the promotion, so nothing jumps when it arrives.
One thing wasn't fixed. Each click on Add to cart still sorts those 200,000 prices and draws all 1,800 teas. The fast store scored 100 in all 5 runs:
// The fast store: each metric's score, the same in all five runs.const scores = { FCP: 1, SI: 1, LCP: 0.98, TBT: 1, CLS: 1 };const weights = { FCP: 10, SI: 10, LCP: 25, TBT: 30, CLS: 25 }; let points = 0;for (const metric in weights) points += scores[metric] * weights[metric];console.log(`The fast store kept ${points} of 100 points.`);console.log(`The report shows ${Math.round(points)}.`);- Line 2 holds the fast store's metric scores. They were the same in every run, and only LCP fell short of 1.
- Line 7 prints
The fast store kept 99.5 of 100 points. - Line 8 rounds. 99.5 rounds up, so it prints
The report shows 100.
Even the 100 wasn't perfect: it rounded up from 99.5. Now look at the click the score never saw, from the timespan:
// The fast store's Add to cart click, timed in a Lighthouse timespan.const inp = 302.1; // INP's limits: 200 ms or less is good, and over 500 ms is poor.const rating = inp <= 200 ? "good" : inp <= 500 ? "needs improvement" : "poor";console.log(`Add to cart took ${inp} ms to respond: ${rating}.`);- Line 2 is the click's recorded INP.
- Line 5 rates it with INP's limits. Lighthouse's INP audit uses the same 200 ms and 500 ms as the tools that measure real users.
- Line 6 prints
Add to cart took 302.1 ms to respond: needs improvement.
A 100, and a click that needs improvement. Remember the tea shop before it opens: the score watched a load with no customers. It also used one pretend phone on one pretend network. Your users bring other phones, other networks, and their own clicks. A 100 is a perfect mark on one test, on one day. It's real, but it isn't the whole report.
Chrome's docs don't ask for a 100 anyway: "A 'perfect' score of 100 is extremely challenging to achieve and not expected." They suggest that "it might be more useful to think of your site performance as a distribution of scores, rather than a single number."
Why it matters: use the score to compare versions of your page under the same conditions. To judge what your users go through, use field data, as Lab data vs field data showed, and the limits from Core Web Vitals.
Does it move the score?
You now know what goes into the score: five metrics, measured during one page load. For each change below, ask one question. Does it change one of those five, during the load? Then sort it into one of two piles:
- LCP falls from 2.8 s to 1.8 s
- A 300 ms task runs during the load
- The promotion pushes the page down while it loads
- Add to cart takes 300 ms to respond
- An insight fails, but no metric changes
- An image loses its alt text
Sort each change by whether it moves the Performance score of a navigation.
Common mix-ups
- "Fixing a failing insight raises the score." No. Insights weigh 0. The score moves only if the fix changes one of the five metrics.
- "Every metric counts the same." No. TBT counts 30, LCP and CLS 25 each, and FCP and Speed Index only 10.
- "Every millisecond is worth the same." No. The curve is steep in the middle and flat at the top. The same 500 ms raised LCP's score by 0.13 in the middle and by 0.03 near the top.
- "A one-point drop means the page got slower." No. The store scored 89 and 90 with nothing changed. Compare the medians of several runs.
- "INP is part of the score." No. A navigation doesn't measure it, and a timespan gives it a weight of 0. TBT stands in for it.
- "100 means fast for everyone." No. The fast store scored 100 with a 302 ms click, on one pretend phone.
Practice exercises
Answer first, then use the hints.
Lighthouse also ran the store with its desktop settings. What does the last line print?
// The store's first desktop run: each metric's score, from 0 to 1.
const scores = { FCP: 1, SI: 1, LCP: 0.99, TBT: 1, CLS: 0.97 };
// How much each metric counts. The weights add up to 100.
const weights = { FCP: 10, SI: 10, LCP: 25, TBT: 30, CLS: 25 };
let points = 0;
for (const metric in weights) points += scores[metric] * weights[metric];
console.log(`The store kept ${points.toFixed(2)} of 100 points.`);
console.log(`The report shows ${Math.round(points)}.`);The report shows 99. LCP and CLS lost about a point between them, and 99.00 rounds to 99.
The store's LCP was 2,693 ms. This code reads it off the desktop curve, with score() from the curves section. What does it print?
// score() is Lighthouse's curve, from the curves section.
// Desktop runs use their own two points for LCP.
const lcp = { p10: 1200, median: 2400 };
console.log(`On desktop, an LCP of 2693 ms scores ${score(lcp, 2693)}.`);On desktop, an LCP of 2693 ms scores 0.41. On the mobile curve, the same LCP scores 0.85. Desktop's curve expects a faster computer, so it's stricter.
In the first playground, set TBT to 600 ms and leave the other values. What's the Performance score?
75. TBT scores 0.5, so it keeps 15 of its 30 points. With the other four as before, that's 74.65 points, shown as 75.
In the second playground, pick Quiet machine. Which of the five metrics had the same value in all ten runs?
CLS: 0.136 in every run. The page jumped the same way each time. Only the timings wobbled, and those wobbles are what moved the score.
Open DevTools, run Lighthouse on any page in its default navigation mode, and look under the Performance score. A short note ends with a link to a tool that works out the score from the metric values. What does the link say?
See calculator. It opens Lighthouse's scoring calculator with your run's values filled in. Change a value there to see what a fix would do to the score before you make it.
Lighthouse's variability docs name the simplest way to run Lighthouse several times and get the median run. Which tool is it?
Lighthouse CI. lhci collect with -n 5 runs Lighthouse five times, saves every report, and marks the median run, so you compare like with like.
Check your understanding
For each question, ask which metrics count, how much, and whether anything really changed.
Question 1 of 7What does the last line of the store's weights example print?
Read the code, then predict// The store's first run: each metric's score, from 0 to 1. const scores = { FCP: 0.86, SI: 0.98, LCP: 0.85, TBT: 1, CLS: 0.8 }; // How much each metric counts. The weights add up to 100. const weights = { FCP: 10, SI: 10, LCP: 25, TBT: 30, CLS: 25 }; let points = 0; for (const metric in weights) points += scores[metric] * weights[metric]; console.log(`The store kept ${points.toFixed(2)} of 100 points.`); console.log(`The report shows ${Math.round(points)}.`);Choose an answer to see the explanation.
Question 2 of 7What does the last line of the fast store's example print?
Read the code, then predict// The fast store: each metric's score, the same in all five runs. const scores = { FCP: 1, SI: 1, LCP: 0.98, TBT: 1, CLS: 1 }; const weights = { FCP: 10, SI: 10, LCP: 25, TBT: 30, CLS: 25 }; let points = 0; for (const metric in weights) points += scores[metric] * weights[metric]; console.log(`The fast store kept ${points} of 100 points.`); console.log(`The report shows ${Math.round(points)}.`);Choose an answer to see the explanation.
Question 3 of 7Which metric weighs the most in Lighthouse's Performance score?
Choose an answer to see the explanation.
Question 4 of 7A page makes its LCP 500 ms faster. Where on the curve does that raise its score the most?
Choose an answer to see the explanation.
Question 5 of 7Why isn't INP part of a navigation's Performance score?
Choose an answer to see the explanation.
Question 6 of 7Two Lighthouse runs of the same page score 89 and 90. What do you conclude?
Choose an answer to see the explanation.
Question 7 of 7A page scores 100 in Lighthouse. What can still be slow?
Choose an answer to see the explanation.
Key takeaways
- The Performance score is a weighted average of five metric scores: TBT 30, LCP 25, CLS 25, FCP 10 and Speed Index 10. Insights weigh 0.
- Each metric's score is read off a log-normal curve, set by real sites in HTTP Archive. It's steep in the middle and flat at the top.
- INP isn't in a navigation's score, because nobody clicks during a page load. TBT stands in for it.
- Scores wobble between runs with nothing changed. Run five times and compare medians.
- A 100 sums up one lab load. Clicks, and real users' phones and networks, can still be slow.
Remember the one-liner.
Five values, five curves, one weighted average: a report on one load, not a verdict on your site.
Sources
The facts on this page come from these docs and source files:
- Lighthouse performance scoring, Chrome for Developers.
- Speed Index, Chrome for Developers.
- default-config.js, the weights, Lighthouse 13.6.0 source.
- scoring.js, Lighthouse 13.6.0 source.
- Frequently asked questions, HTTP Archive.
- statistics.js, Lighthouse 13.6.0 source.
- util.js, Lighthouse 13.6.0 source.
- largest-contentful-paint.js, a metric's control points, Lighthouse 13.6.0 source.
- Interaction to Next Paint (INP), Jeremy Wagner and Barry Pollard, web.dev.
- interaction-to-next-paint.js, Lighthouse 13.6.0 source.
- Network Throttling, Lighthouse docs.
- config.js, Lighthouse 13.6.0 source.
- Architecture, Lighthouse docs.
- Total Blocking Time (TBT), Philip Walton and Barry Pollard, web.dev.
- Score Variability, Lighthouse docs.
- report-utils.js, the report's own words, Lighthouse 13.6.0 source.
Coming next: Throttling and simulation, also in Measuring in the lab. How Lighthouse imitates a slow phone, what Lantern works out, and why PageSpeed Insights disagrees with your laptop.