How Lighthouse works
What Lighthouse does when it tests a page, step by step, and how its three modes, navigation, timespan, and snapshot, measure different things. Recorded with Lighthouse 13.6.0 on the course's store.
- 01Run it four waysStart Lighthouse in DevTools, from the command line, from Node, or on PageSpeed Insights, and say whose computer did the work.
- 02Follow one runFollow a run from the blank page to the report, and say what it did to your page and to your server.
- 03Tell gathering from auditingExplain how gatherers collect artifacts and audits judge them, and why the same artifacts always get the same score.
- 04Pick the right modeChoose navigation, timespan, or snapshot for the question you're asking, and put them together in a user flow.
What Lighthouse does
Asha told Leaf and Kettle that its page is slow on her phone. Over the last few lessons, you recorded the store in the Performance panel and found what was likely slowing it down. Before you change anything, you want one number. Something you can check again after each fix, to see if it helped.
Chrome has a tool that gives you one: Lighthouse. The course ran it on the store, with its default settings. Seven seconds later, it gave the store a Performance score of 90 out of 100.
But Asha's phone is still slow. So what exactly scored 90? Which phone was the page tested on? Which network? Did anyone tap Add to cart? This lesson follows that one run from start to finish, to find out.
Lighthouse is an open-source tool that tests web pages. It's built into Chrome's DevTools, and you can also run it on its own. You give it an address. It loads the page in Chrome, runs a long list of checks on what it saw, and writes a report. The report covers more than speed: the store also got scores for Accessibility, Best Practices and SEO.
Here is the whole run at once, drawn to scale:
The same four parts, as numbers you can add up:
// One Lighthouse run on the store, in four parts.// Each time is in milliseconds (ms), from Lighthouse's own notes.const run = { gettingReady: 1102.5, loading: 3150.5, collecting: 2722.8, auditing: 266.6 }; const total = Object.values(run).reduce((sum, ms) => sum + ms, 0);console.log(`The whole run took ${(total / 1000).toFixed(1)} seconds.`);console.log(`Loading the page took ${Math.round((run.loading / total) * 100)}% of it.`);console.log(`Auditing took ${Math.round((run.auditing / total) * 100)}% of it.`);- Lines 1 and 2 are comments. Every time is in milliseconds (ms), thousandths of a second, from Lighthouse's own notes.
- Line 3 holds the four parts.
gettingReadyis everything before the page starts loading.collectingis the work Lighthouse does on the page after it has loaded. - Line 5 adds the four parts up.
- Lines 6 to 8 print
The whole run took 7.2 seconds.,Loading the page took 44% of it.andAuditing took 4% of it.
So the page itself was loading for less than half of the run. The rest went on getting ready, and on studying the page once it had loaded. Auditing is the part that checks what Lighthouse saw and works out the scores. It took 4%. The first three parts need Chrome. The last part doesn't need a browser at all.
Before you buy a house, you can pay an inspector to look at it. They come once, on a day you agree. They test every house the same way: run every tap, switch on every light. They bring the same checklist every time. Then they write a report. It tells you how the house did on that day. It can't tell you what living there is like all year.
- In real life: The house
- In Lighthouse: Your page
- In real life: One visit, on a day you agree
- In Lighthouse: One load of the page, at one time
- In real life: Testing every house the same way
- In Lighthouse: Fixed settings: a phone-sized screen, an empty cache, a set phone and network speed
- In real life: The same checklist for every house
- In Lighthouse: The audits: the same list of checks for every page
- In real life: The written report
- In Lighthouse: The Lighthouse report, with its scores
Lighthouse is a lab test. It loads your page once, under settings it chooses, writes down what it saw, and then checks those notes against a list. Its numbers describe that one load, on that one machine. They aren't a measurement of Asha's phone.
That's what Lab data vs field data called a lab test: a test under conditions someone chose, not a measurement of real visits. The sections below take this one run apart.
Four ways to run Lighthouse
There are four ways to start Lighthouse. They all run the same tests. They differ in where you start it, and whose computer does the work:
- In Chrome DevTools. Open DevTools, pick the Lighthouse panel, and click Analyze page load. It tests the page in the browser you're using. So it works on pages you have to sign in to see.
- From the command line. Type
npx lighthouseand the page's address. This suits scripts that you run again and again. - As a Node module. Call Lighthouse from your own JavaScript. Chrome's docs suggest this for continuous integration: the checks a team runs automatically each time the code changes.
- On PageSpeed Insights. It's a web page from Google. Paste an address and click Analyze. There's nothing to install.
The command line and Node need Google Chrome installed on your computer. Lighthouse doesn't load pages by itself. It drives Chrome. From Node, a run takes a few lines:
import lighthouse from "lighthouse";import * as chromeLauncher from "chrome-launcher"; const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless=new"] });const { lhr } = await lighthouse("http://127.0.0.1:8080/", { port: chrome.port }); console.log(lhr.lighthouseVersion, lhr.categories.performance.score);await chrome.kill();- Line 1 loads Lighthouse. Line 2 loads
chrome-launcher, a small package that starts Chrome for you. - Line 4 starts Chrome without a window. That's what
--headless=newasks for. - Line 5 runs Lighthouse on the store. It passes Chrome's debugging port: a numbered door that lets another program drive Chrome, open pages, and read what happened.
- Line 7 prints two things from
lhr, the Lighthouse Result: one big object holding everything the run found. Here, the version, and the Performance score from 0 to 1. - Line 8 closes Chrome.
The course's recorder makes these same calls. Its run printed version 13.6.0 and a score of 0.9. The report shows that as 90.
PageSpeed Insights runs it for you
PageSpeed Insights is different in one big way. It runs Lighthouse on Google's computers, not yours. Its docs say it runs "in a Google datacenter", in North America, Europe or Asia. So its lab numbers describe Google's machine and network, not your laptop.
It also shows a second kind of number: field data. These are real visits by Chrome users who agreed to share them, collected in the Chrome User Experience Report, or CrUX. PageSpeed Insights shows what those visits got over the previous 28 days.
The weather page on your phone shows two different things. One is tomorrow's forecast, worked out by a computer model. The other is what yesterday was really like: how warm it got, and whether it rained. A sunny forecast doesn't mean yesterday was dry. You read both, and you know which is which.
- In real life: Tomorrow's forecast, worked out by a computer model
- In PageSpeed Insights: The Lighthouse result: one load, on a set phone and network
- In real life: What the weather really was yesterday
- In PageSpeed Insights: Field data: what real Chrome users got over 28 days
- In real life: One weather page that shows both
- In PageSpeed Insights: PageSpeed Insights, with field data and a Lighthouse run
Two things follow. If PageSpeed Insights and your own DevTools run give different lab scores, nothing is broken: they ran on different machines. Throttling and simulation compares results across machines. And if the lab score is green but the field data isn't, the field data is what your visitors really got. Judge the page by it, as Lab data vs field data explained.
What Lighthouse does to your page
Back to the store's run. Lighthouse keeps notes as it works: every step, when it started, and how long it took. These timing entries are saved in the Lighthouse Result. The course also logged every request the store's servers received. Side by side, the two show exactly what Lighthouse did to the store.
Pick a step in the playground to see how long it took, and which requests reached the servers during it:
/200, the phone/styles.css200, the phone/fonts.css200, the phone/app.js200, the phone/hero.jpg200, the phone/widget.js200, the phone, the widget's server/reviews.js200, the phone/geist.woff2200, the phone
It loads the page. Then it waits until 1,000 ms have passed since the load event, and the network and the CPU have each been quiet for 1,000 ms. This took 3,150.5 ms. All 8 of the page's files arrived in this step, each with the phone's user agent.
Getting ready: about a second
Before it loads the store, Lighthouse does four things, in this order:
- It opens a blank page,
about:blank. It turns its network log off while it does, so nothing from before the run gets into its notes. - It rates the computer. It runs a benchmark: a small test program that counts how much work the CPU gets done. You met a version of it in The Performance panel, where DevTools used it to calibrate its slowdown.
- It makes Chrome behave like a phone. The screen becomes 412 by 823 CSS pixels. The user agent, the line of text a browser sends with each request to say what it is, now names an Android phone, a moto g power.
- It clears out saved data for the store, so that its files have to come over the network again.
The benchmark is most of this part: 1,004.6 ms of it. It runs two half-second loops of JavaScript that build strings and copy arrays. The more loops the CPU gets through, the higher the score. This computer scored 3,001, and Lighthouse's source calls 1,000 and up "a desktop-class device". The report lists the score as "Unthrottled CPU/Memory Power", so readers know how fast the testing computer was.
The reset works on the store's origin: its scheme, host and port together, like http://127.0.0.1:8080. Here is what it clears by default:
file_systemsshader_cacheservice_workerscache_storage- and, separately, the browser's cache.
Clearing them matters for speed. A service worker, a script a site installs to answer its own requests, could answer the page's requests from files saved in Cache Storage. So could the browser's cache. With those cleared, every file has to come over the network, as on a first visit.
Cookies, localStorage and IndexedDB aren't on the list, so they survive the reset. That's how the DevTools panel can test a page you're signed in to: your cookies are still there. You can also turn the whole reset off with a setting. Exercise 5 asks you to find it.
One thing is missing from the list: slowing down. In The Performance panel, DevTools slowed the computer down, so it behaved like a phone while the page loaded. Lighthouse's default method, simulate, doesn't. It loads the page at this computer's full speed. Afterwards, it calculates how long the same load would have taken on a slower phone, on a slower network.
That phone's CPU is 4 times slower than this computer's. Its network takes 150 ms for a round trip, a message to the server and back, and carries about 1.6 megabits a second. Throttling and simulation shows how the calculation works. For now, remember the forecast: Lighthouse's speed numbers are worked out, not timed on a real phone.
Loading the page: about three seconds
Now Lighthouse loads the store, and the store's servers see the page arrive. Here are the requests they received during this step:
// The requests the store's server got while Lighthouse loaded the page.// Each is [file, when it arrived], in ms from the start of the run.const loadStartMs = 1102.5;const requests = [ ["/", 1107.7], ["/styles.css", 1844], ["/fonts.css", 1844.1], ["/app.js", 1844.6], ["/hero.jpg", 1844.7], ["/widget.js", 1847.7], ["/reviews.js", 1848.2], ["/geist.woff2", 1860.3],]; const lastMs = requests.at(-1)[1] - loadStartMs;console.log(`The page asked for ${requests.length} files.`);console.log(`The last one arrived ${lastMs.toFixed(1)} ms into the load.`);- Lines 1 and 2 are comments. Each request is a file and the moment it reached the server, counted from the start of the run.
- Line 3 is when the load step began: 1,102.5 ms into the run.
- Lines 5 and 6 are the 8 requests, as recorded. Every one of them carried the phone's user agent.
- Line 9 works out how far into the step the last request came.
- It prints
The page asked for 8 files.andThe last one arrived 757.8 ms into the load.
Look at the first two times on line 5. The request for the page itself arrived at 1,107.7 ms. The next one came at 1,844 ms. In between, the store's server held the HTML back for 700 ms: the slow server from Performance insights. The browser couldn't ask for the other files until the HTML told it what they were.
Now compare the 757.8 ms with the whole step, which lasted 3,150.5 ms. For its last 2.4 seconds, the store's servers heard nothing new. Lighthouse was letting the page finish, and then waiting for it to go quiet. Why wait? A page that has stopped asking for files can still be running scripts or drawing. Lighthouse needs to know when the page is really done.
At a family photo, the photographer doesn't press the button the moment the last cousin arrives. They wait until nobody is walking in or out of the picture. Then they wait until everyone stops fidgeting. If a small child just won't keep still, they take the photo anyway, after a while.
- In real life: Everyone has arrived
- In Lighthouse: The page's load event has fired
- In real life: Nobody is walking in or out of the picture
- In Lighthouse: The network is quiet: two requests or fewer at a time
- In real life: Nobody is fidgeting
- In Lighthouse: The CPU is quiet: no long tasks on the main thread
- In real life: Taking the photo anyway, after a long wait
- In Lighthouse: Giving up after 45 seconds
Lighthouse's source spells out what "keeping still" means. With the default settings, it waits until all of these are true:
- 1,000 ms have passed since the page's load event, the moment the browser says the page and its files have loaded.
- The network has been quiet for 1,000 ms. Quiet means two requests or fewer still going.
- After that, the CPU has been quiet for 1,000 ms. Quiet means no long task: no single piece of work that kept the main thread busy for 50 ms or more.
If that never happens, it stops waiting after 45 seconds and carries on with what it has. Why it matters: the load step includes the work your page does right after it loads. A script that keeps the main thread busy after the load event makes Lighthouse wait longer, and that work ends up in the recording.
Collecting the rest: under three seconds
With the page settled, Lighthouse asks it questions and saves each answer: its links, its images, its scripts and more. It also saves the trace: the full recording of the load, the same kind you read in Reading a performance trace.
Two of these steps do things you might not expect:
- It takes a screenshot of the whole page, top to bottom, for the report. After the load itself, it's the longest step in the run. Exercise 3 asks you to find out how long it took.
- It tests the back/forward cache. That's a browser feature that keeps a whole page in memory when you leave it, so the Back button can show it at once. Lighthouse goes to
chrome://terms, a page built into Chrome, and then back. The store came straight back from the cache, so that check passed.
The servers noticed this part too. Here is what they received after the load:
// What the store's server got after the load, in order: [file, who asked, status].// "phone" is Chrome still dressed as the phone. "chrome" is Chrome as itself.const afterLoad = [ ["/llms.txt", "chrome", 404], ["/styles.css", "phone", 200], ["/fonts.css", "phone", 200], ["/robots.txt", "chrome", 404], ["/", "chrome", 200], ["/.well-known/ard.json", "chrome", 404], ["/.well-known/ai-catalog.json", "chrome", 404],]; const again = afterLoad.filter(([, by]) => by === "phone").map(([file]) => file);const own = afterLoad.filter(([, by]) => by === "chrome").map(([file, , status]) => `${file} ${status}`);console.log(`Asked for again: ${again.join(", ")}`);console.log(`Chrome itself: ${own.join(", ")}`);- Lines 1 and 2 are comments.
"phone"means the request carried the phone's user agent."chrome"means Chrome asked as itself, with its own user agent. - Lines 4 to 6 are the 7 requests, in the order they arrived, with the status the server sent back. A 404 means "not found".
- Line 9 picks the files the phone asked for. Line 10 picks the ones Chrome asked for itself.
- Lines 11 and 12 print
Asked for again: /styles.css, /fonts.cssandChrome itself: /llms.txt 404, /robots.txt 404, / 200, /.well-known/ard.json 404, /.well-known/ai-catalog.json 404.
So two kinds of request came in after the load:
- The page's two stylesheets, again. They came during the step that collects the page's CSS, called CSSUsage.
- Five requests from Chrome itself. They feed Lighthouse's checks, not the page.
robots.txtis for an SEO check.llms.txt, the page, and the two files under/.well-known/are for the checks in a category called Agentic Browsing.
None of these requests are part of the page loading, so none of them count in its metrics. But they all show up in your server's logs. If your logs show requests for robots.txt and llms.txt right after a test, that's likely Lighthouse, not a visitor.
Gatherers, audits, and the report
You've now seen Lighthouse do two kinds of work. While the page was open, it watched and wrote things down. At the very end, in 266.6 ms, it judged. An inspector's clipboard makes them easy to picture:
During the visit, the inspector only measures and writes things down. The damp in the bathroom, the age of the boiler, a photo of the roof: it all goes on the clipboard. Back at the office, they go through their list of checks, using only what's on the clipboard. Anyone at the office could do the checks from that clipboard and get the same answers.
- In real life: Taking one reading, like the damp in the bathroom
- In Lighthouse: A gatherer collecting one kind of information
- In real life: The clipboard, full of readings and photos
- In Lighthouse: The artifacts
- In real life: One check on the office's list: is the damp too high?
- In Lighthouse: One audit
- In real life: A colleague doing the checks from the same clipboard
- In Lighthouse: Audit mode,
-A, run on saved artifacts - In real life: The finished report
- In Lighthouse: The report, drawn from the Lighthouse Result
Here are the parts, in Lighthouse's words:
- A gatherer collects one kind of information from Chrome. The docs say it does "Minimal post-processing." It writes things down. It doesn't judge.
- What a gatherer collects is an artifact: the trace, the list of images, the robots.txt file, and so on.
- An audit is one check, such as "Do the images have a width and height?" In the docs' words: "Using the Artifacts as input, an audit evaluates a test and resolves to a numeric score."
- The Lighthouse Result holds every audit's result. The report you read is drawn from it.
The store's run had 40 artifacts. Lighthouse filled in 12 of them itself, such as its settings and the benchmark score. Gatherers collected the other 28.
Fewer checks, less to collect
A category is one section of the report, such as Performance or SEO. When you ask for fewer categories, Lighthouse collects fewer artifacts. The course ran both ways three times, taking turns:
// Every category, and only Performance: three runs each, taking turns.const runs = { "every category": { artifacts: 40, audits: 161, gatherMs: [7250.5, 6975.8, 6960.1] }, "Performance only": { artifacts: 25, audits: 49, gatherMs: [6149, 6041.5, 6038] },}; for (const [name, { artifacts, audits, gatherMs }] of Object.entries(runs)) { const median = [...gatherMs].sort((a, b) => a - b)[1]; console.log(`${name}: ${artifacts} artifacts, ${audits} audits, gathering ${median} ms`);}- Lines 3 and 4 are recorded: how many artifacts and audits each kind of run had, and how long gathering took in each of its three runs.
- Line 8 sorts the three times and takes the middle one, the median.
- It prints
every category: 40 artifacts, 161 audits, gathering 6975.8 msandPerformance only: 25 artifacts, 49 audits, gathering 6041.5 ms.
Asking for Performance alone skipped 15 artifacts and made gathering about 900 ms shorter. If speed is all you're testing, that's almost a second saved on every run.
Gathering and auditing can run apart
Audits read only the artifacts. So Lighthouse can stop halfway, and finish later. Gather mode, -G, collects the artifacts and saves them to disk. Audit mode, -A, reads saved artifacts and runs the audits, with no browser at all. The course gathered once. Here is what it saved:
// The files Lighthouse's gather mode saved for one run, in bytes.const saved = { "artifacts.json": 805308, "devtoolslog.json": 45836, "trace.json": 3142201 }; const total = Object.values(saved).reduce((sum, bytes) => sum + bytes, 0);console.log(`saved: ${(total / 1e6).toFixed(1)} MB`);console.log(`the trace alone: ${Math.round((saved["trace.json"] / total) * 100)}%`);- Line 2 holds the three files gather mode saved, with their sizes in bytes, as recorded.
- Line 4 adds the sizes up.
- Lines 5 and 6 print
saved: 4.0 MBandthe trace alone: 79%.
Lighthouse's docs describe each file:
artifacts.jsonholds "The output from all gatherers."trace.jsonis the trace. "Most performance characteristics come from here," and you can open it in the Performance panel.devtoolslog.jsonis a log of the messages Chrome sent Lighthouse during the run. The docs call it the "Primary signal about network requests and page state."
So the trace, the security camera footage from The Performance panel, is most of the clipboard. Then the course audited those same files twice:
// Audit mode, run twice on the same saved files, with no browser.const audits = [{ ms: 498.9, score: 0.9, lcpMs: 2686.4 }, { ms: 451.2, score: 0.9, lcpMs: 2686.4 }]; for (const [index, { ms, score, lcpMs }] of audits.entries()) { console.log(`audit ${index + 1}: score ${Math.round(score * 100)}, LCP ${lcpMs} ms, in ${ms} ms`);}- Line 2 holds the two audit runs, as recorded: how long each took, the Performance score, and the LCP. LCP, Largest Contentful Paint, is the moment the biggest image or block of text appeared.
- Line 5 prints one line per run:
audit 1: score 90, LCP 2686.4 ms, in 498.9 msandaudit 2: score 90, LCP 2686.4 ms, in 451.2 ms.
Same artifacts, same score, same LCP, to the decimal. Only the time each audit run took changed. Now compare the three full runs from earlier: they scored 88, 90 and 90. So when the score moves from run to run, the change comes from gathering. Each run loads the page again, and sees something a little different. The next lesson looks at why.
From the command line, the same runs look like this:
npx lighthouse http://127.0.0.1:8080/npx lighthouse http://127.0.0.1:8080/ --only-categories=performancenpx lighthouse http://127.0.0.1:8080/ -Gnpx lighthouse http://127.0.0.1:8080/ -A- Line 1 runs every category. Line 2 runs only Performance.
- Line 3 gathers and saves, to a folder called
latest-rununless you name another. - Line 4 audits what line 3 saved. It needs no browser.
Navigation, timespan, and snapshot
Every run so far has started the same way: with a fresh page load. But Asha had two complaints. The picture of mint tea was slow to appear. And when she tapped Add to cart, for a moment nothing happened. A page-load test can't see that tap. It's over before anyone taps anything.
So Lighthouse has three modes, for three kinds of question. To picture them, go back to the security camera above Leaf and Kettle's counter, from The Performance panel:
You can use the shop's camera three ways. Film opening time, starting as the shutter goes up. Film one customer, from the moment they walk in until they leave with their tea. Or take one photo of the counter, to see what's on it right now. A photo can't tell you how long anything took. But it shows exactly what's there.
- In real life: Filming opening time, from the shutter going up
- In Lighthouse: Navigation mode: one page load
- In real life: Filming one customer, from the door to the till
- In Lighthouse: Timespan mode: the stretch of time you choose, usually with a click or a tap
- In real life: One photo of the counter
- In Lighthouse: Snapshot mode: the page as it is at one moment
- In real life: A photo has no running time
- In Lighthouse: A snapshot has no metrics and no score
Lighthouse's docs define the three modes in one line each:
- "Navigation mode analyzes a single page load." It's the default, and every run in this lesson so far used it.
- "Timespan mode analyzes an arbitrary period of time, typically containing user interactions." You start it, use the page, and stop it.
- "Snapshot mode analyzes the page in a particular state", such as with a menu open.
DevTools' Lighthouse panel has all three: you pick the mode, then click its button. Read each row of this table across: one mode, what it tests, and how its report shows Performance.
| Mode | What does it test? | How does it show Performance? | When would you use it? |
|---|---|---|---|
| Navigation | It tests one page load, until the page goes quiet. | It shows a score out of 100. | Use it to test how a page loads. |
| Timespan | It tests whatever happens between its start and its end. | It shows the audits that passed, like 17/21. | Use it to test a click, a tap, or typing. |
| Snapshot | It tests the page as it is at one moment. | It shows the audits that passed, like 0/1. | Use it to test an open menu or a filled-in form. |
The docs also list each mode's limits. A timespan "Cannot analyze moment-based performance metrics (e.g. Largest Contentful Paint)", and has no Accessibility category. A snapshot doesn't look at the network or the main thread, so it has no metrics at all.
The course ran all three on the store, one after the other: load it, tap Add to cart, then look at the page. Here is what each step reported:
// The store's user flow: how many audits each step ran, and which metrics it reported.const flow = [ { mode: "navigation", audits: 161, metrics: ["FCP", "LCP", "TBT", "CLS", "SI"] }, { mode: "timespan", audits: 49, metrics: ["TBT", "CLS", "INP"] }, { mode: "snapshot", audits: 90, metrics: [] },]; for (const { mode, audits, metrics } of flow) { console.log(`${mode}: ${audits} audits, ${metrics.length > 0 ? metrics.join(", ") : "no metrics"}`);}- Lines 3 to 5 are recorded, from each step's report: how many audits ran, and which metrics it showed.
- Line 9 prints one line per step:
navigation: 161 audits, FCP, LCP, TBT, CLS, SI,timespan: 49 audits, TBT, CLS, INPandsnapshot: 90 audits, no metrics.
Here is each of those metrics in one line:
- FCP, First Contentful Paint: when the first text or image appeared.
- LCP, Largest Contentful Paint: when the biggest image or block of text appeared.
- TBT, Total Blocking Time: how long the main thread was blocked for long enough that it couldn't answer input.
- CLS, Cumulative Layout Shift: how much things moved around on the screen.
- SI, Speed Index: how quickly the page's content showed up on the screen.
- INP, Interaction to Next Paint: how quickly the page answered a click, a tap or a key press.
Only the timespan measured the click. It reported INP, the click's metric, and no LCP. Why it matters: to measure a tap like Asha's, you need a timespan. A plain page load doesn't include one.
Pick a mode to see what the store's flow report showed for it:
A timespan tests whatever happens between its start and its end: here, the click on Add to cart. 49 audits ran. There's no LCP: the docs say a timespan can't measure moment-based metrics like it. There is an INP: 270 ms. Performance shows 17/21: the audits that passed, out of those that count. These didn't pass: total-blocking-time, interaction-to-next-paint, inp-breakdown-insight, unsized-images.
Look at Performance in the timespan: 17/21, not a score. For timespans and snapshots, the report shows how many audits passed, out of those that count. So don't compare a timespan's 17/21 with a navigation's score. They're different kinds of number.
INP and CLS in a timespan are lab readings. Real browsers report the same two things to a page's JavaScript, as these two kinds of performance entries:
User flows
A user flow puts several steps, in any of the three modes, into one report. It's like keeping the opening film, the customer clip and the photo of the counter in one folder, in the order they happened.
In DevTools, each run makes its own report. The docs say: "Analyzing a multi-step user flow is currently only available using the Lighthouse Node API along with Puppeteer." Puppeteer is a library that drives Chrome from code: it opens pages, clicks buttons, and types. Here is the store's flow, as the course ran it:
import * as chromeLauncher from "chrome-launcher";import puppeteer from "puppeteer-core";import { startFlow } from "lighthouse"; const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless=new"] });const browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${chrome.port}` });const page = await browser.newPage();const flow = await startFlow(page); await flow.navigate("http://127.0.0.1:8080/", { name: "Load the store" });await flow.startTimespan({ name: "Add to cart" });await page.click("#add");await new Promise((resolve) => setTimeout(resolve, 2000));await flow.endTimespan();await flow.snapshot({ name: "After adding to cart" }); const report = await flow.generateReport();- Lines 1 to 3 load chrome-launcher, Puppeteer, and Lighthouse's
startFlow. - Lines 5 to 7 start Chrome, connect Puppeteer to it, and open a tab.
- Line 8 starts the flow in that tab.
- Line 10 is the navigation: it loads the store.
- Lines 11 to 14 are the timespan. Line 11 starts it. Line 12 clicks Add to cart. Line 13 waits two seconds. Line 14 ends it.
- Line 15 is the snapshot: the page as the click left it.
- Line 17 builds one report with all three steps.
Line 13 matters. The docs advise: "Always wait for transitions and interactions to finish before ending a timespan." The two seconds leave time for the work the click starts to happen inside the timespan.
One more tip from the docs: "strive for each report to have a narrow focus." One load, one interaction, one state. Then each report answers one question. To see the real thing, open the store's flow report (1.4 MB), the file Lighthouse wrote for this flow.
Which mode?
Each item below is something you might want to test. For each one, ask: is it about a page load, about something that happens over time, or about the page at one moment?
- The LCP of your landing page
- A Performance score out of 100
- The INP of clicking Add to cart
- Layout shifts while a filter updates the list
- Accessibility of a menu once it's open
- Images without sizes inside an open dialog
Sort each item by the Lighthouse mode that tests it.
Common mix-ups
- "Lighthouse loads my page on a slow phone." No. By default, it loads the page at this computer's full speed, dressed up as a phone. Then it calculates how long a slower phone would have taken.
- "It only loads my page." No. It also opens a blank page, goes to chrome://terms and back, and asks your server for robots.txt, llms.txt and more.
- "It starts from a clean browser." No. It clears the cache and some kinds of storage. Cookies and localStorage stay, so you stay signed in.
- "A timespan's 17/21 is a score." No. It's the number of audits that passed, out of those that count.
- "Auditing is the slow part." No. On the store, gathering took about seven seconds. All 161 audits took 266.6 ms.
- "PageSpeed Insights tested my page on my computer." No. It runs Lighthouse on Google's computers, in a datacenter.
Practice exercises
Try each one before you open the hints.
This is the four-parts example, with loading 3,000 ms longer. What does line 7 print now?
// One Lighthouse run on the store, in four parts.
// Each time is in milliseconds (ms), from Lighthouse's own notes.
const run = { gettingReady: 1102.5, loading: 6150.5, collecting: 2722.8, auditing: 266.6 };
const total = Object.values(run).reduce((sum, ms) => sum + ms, 0);
console.log(`The whole run took ${(total / 1000).toFixed(1)} seconds.`);
console.log(`Loading the page took ${Math.round((run.loading / total) * 100)}% of it.`);
console.log(`Auditing took ${Math.round((run.auditing / total) * 100)}% of it.`);Loading the page took 60% of it. The load grew by 3,000 ms, and so did the total. So the load's share went up, from 44% to 60%.
This is the after-the-load example, for a store that has a robots.txt file. What does line 12 print?
// What the store's server got after the load, in order: [file, who asked, status].
// "phone" is Chrome still dressed as the phone. "chrome" is Chrome as itself.
const afterLoad = [
["/llms.txt", "chrome", 404], ["/styles.css", "phone", 200], ["/fonts.css", "phone", 200],
["/robots.txt", "chrome", 200], ["/", "chrome", 200],
["/.well-known/ard.json", "chrome", 404], ["/.well-known/ai-catalog.json", "chrome", 404],
];
const again = afterLoad.filter(([, by]) => by === "phone").map(([file]) => file);
const own = afterLoad.filter(([, by]) => by === "chrome").map(([file, , status]) => `${file} ${status}`);
console.log(`Asked for again: ${again.join(", ")}`);
console.log(`Chrome itself: ${own.join(", ")}`);Chrome itself: /llms.txt 404, /robots.txt 200, / 200, /.well-known/ard.json 404, /.well-known/ai-catalog.json 404. The server found robots.txt this time, so it answered 200 instead of 404.
In the first playground, how long did the full-page screenshot take?
1,342.3 ms. After the load itself, it's the longest step in the run, longer than the benchmark.
In the second playground, what INP did the timespan measure for the click on Add to cart?
270 ms. INP is good at 200 ms or less, so this click needs improvement. Only the timespan could measure it: it's the only step that contains the click.
You want Lighthouse to leave the cache and storage alone. Which command line flag turns the reset off?
--disable-storage-reset. Its description in cli-flags.js reads: "Disable clearing the browser cache and other storage APIs before a run".
This is the store's flow with one line missing. Now it breaks the docs' advice about timespans. Which line ends the timespan too soon?
import * as chromeLauncher from "chrome-launcher";
import puppeteer from "puppeteer-core";
import { startFlow } from "lighthouse";
const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless=new"] });
const browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${chrome.port}` });
const page = await browser.newPage();
const flow = await startFlow(page);
await flow.navigate("http://127.0.0.1:8080/", { name: "Load the store" });
await flow.startTimespan({ name: "Add to cart" });
await page.click("#add");
await flow.endTimespan();
await flow.snapshot({ name: "After adding to cart" });
const report = await flow.generateReport();import * as chromeLauncher from "chrome-launcher";
import puppeteer from "puppeteer-core";
import { startFlow } from "lighthouse";
const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless=new"] });
const browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${chrome.port}` });
const page = await browser.newPage();
const flow = await startFlow(page);
await flow.navigate("http://127.0.0.1:8080/", { name: "Load the store" });
await flow.startTimespan({ name: "Add to cart" });
await page.click("#add");
await new Promise((resolve) => setTimeout(resolve, 2000));
await flow.endTimespan();
await flow.snapshot({ name: "After adding to cart" });
const report = await flow.generateReport();Line 13 ends the timespan straight after line 12 sends the click, so the work the click starts may fall outside it. Wait before ending it, as the store's flow does with two seconds. Waiting for something on the page, with page.waitForSelector, works too.
Check your understanding
For each question, ask which part of the run did the work, and which mode tests what.
Question 1 of 7What does line 6 of the four-parts example print?
Read the code, then predict// One Lighthouse run on the store, in four parts. // Each time is in milliseconds (ms), from Lighthouse's own notes. const run = { gettingReady: 1102.5, loading: 3150.5, collecting: 2722.8, auditing: 266.6 }; const total = Object.values(run).reduce((sum, ms) => sum + ms, 0); console.log(`The whole run took ${(total / 1000).toFixed(1)} seconds.`); console.log(`Loading the page took ${Math.round((run.loading / total) * 100)}% of it.`); console.log(`Auditing took ${Math.round((run.auditing / total) * 100)}% of it.`);Choose an answer to see the explanation.
Question 2 of 7What does the last line of the modes example print?
Read the code, then predict// The store's user flow: how many audits each step ran, and which metrics it reported. const flow = [ { mode: "navigation", audits: 161, metrics: ["FCP", "LCP", "TBT", "CLS", "SI"] }, { mode: "timespan", audits: 49, metrics: ["TBT", "CLS", "INP"] }, { mode: "snapshot", audits: 90, metrics: [] }, ]; for (const { mode, audits, metrics } of flow) { console.log(`${mode}: ${audits} audits, ${metrics.length > 0 ? metrics.join(", ") : "no metrics"}`); }Choose an answer to see the explanation.
Question 3 of 7You want the INP of opening a menu on a page that has already loaded. Which mode do you use?
Choose an answer to see the explanation.
Question 4 of 7Why does a timespan's report show Performance as something like 17/21 instead of a score?
Choose an answer to see the explanation.
Question 5 of 7With the default settings, what does Lighthouse clear before it loads your page?
Choose an answer to see the explanation.
Question 6 of 7Lighthouse audits the same saved artifacts twice. What happens to the score?
Choose an answer to see the explanation.
Question 7 of 7PageSpeed Insights gives your page a different lab score from the one DevTools gave on your laptop. What's the likely reason?
Choose an answer to see the explanation.
Key takeaways
- Lighthouse is a lab test: one load of your page, under settings it chooses, on one machine.
- You can run it in DevTools, from the command line, from Node, or on PageSpeed Insights. PageSpeed Insights runs it on Google's computers, and shows real users' field data beside it.
- A run gets ready, loads the page, waits for it to go quiet, then collects the rest. Your server also sees requests from Chrome itself.
- By default, Lighthouse loads the page at full speed and calculates a slower phone's numbers afterwards.
- Gatherers collect artifacts. Audits judge only the artifacts, so the same artifacts always get the same score.
- Navigation tests a load, timespan a stretch of time, snapshot one moment. Only a navigation gets a score.
- A user flow puts the modes together in one report, from Node with Puppeteer.
Remember the one-liner.
Lighthouse visits once, writes down what it sees, then checks its notes. Pick the mode that matches your question.
Sources
The facts on this page come from these docs and source files:
- Introduction to Lighthouse, Chrome for Developers.
- About PageSpeed Insights, Google for Developers.
- navigation-runner.js, Lighthouse 13.6.0 source.
- page-functions.js, Lighthouse 13.6.0 source.
- report-renderer.js, Lighthouse 13.6.0 report renderer.
- User-Agent header, MDN.
- constants.js, the default settings, Lighthouse 13.6.0 source.
- wait-for-condition.js, Lighthouse 13.6.0 source.
- PerformanceLongTaskTiming, MDN.
- bf-cache-failures.js, Lighthouse 13.6.0 source.
- Back/forward cache, web.dev.
- Architecture, Lighthouse docs.
- cli-flags.js, Lighthouse 13.6.0 source.
- User Flows in Lighthouse, Lighthouse docs.
- First Contentful Paint (FCP), web.dev.
- Largest Contentful Paint (LCP), web.dev.
- Total Blocking Time (TBT), web.dev.
- Web Vitals, web.dev.
- Speed Index, Chrome for Developers.
- report-utils.js, Lighthouse 13.6.0 report renderer.
Coming next: The Lighthouse score, also in Measuring in the lab. How the metric values become one score from 0 to 100, why that score moves between runs, and what it can't tell you.