From onload to user-centric metrics
Why the load event stopped being enough: the four questions metrics answer, onload and DOMContentLoaded, Speed Index, the retired FMP, TTI and FID, and how a metric becomes a standard.
- 01Ask the four questionsSay which question a metric answers: is it happening, is it useful, is it usable, or is it delightful.
- 02Tell the load events from what people seeShow, with two recorded pages, how DOMContentLoaded and load can fire long before or long after the content appears.
- 03Explain why old metrics were retiredWork out Speed Index, TTI and FID by hand, and say why LCP, TBT and INP took over from FMP, TTI and FID.
Four questions about a page load
Asha is on the bus again. She opens the Tea of the week page at Leaf and Kettle: the blank version from lesson 2.2. The shop's name and the heading appear. Under the heading, the space stays empty. She waits. About three seconds later, the mint tea and its reviews appear at last.
That evening, Ravi checks the page's speed the old way, by its load time. It says the page loaded in 1,225 ms. But Asha waited more than four seconds for her tea. So who is right, the number or Asha?
Both are. They measured different moments. The course recorded this page loading the way Asha's phone loads it. Here are two moments from that load:
// One recorded load of the Tea of the week page, in milliseconds (ms)const loadEvent = 1225;const teaAppeared = 4248; console.log(`The load event fired at ${loadEvent} ms.`);console.log(`The tea appeared at ${teaAppeared} ms.`);console.log(`After the load event, the tea took another ${teaAppeared - loadEvent} ms.`);- Line 1 is a comment. The times are in milliseconds (ms), counted from the start of the load. A millisecond is a thousandth of a second.
- Line 2 is when the page's
loadevent fired. Line 3 is when the tea appeared on the screen. - Lines 5 and 6 print them:
The load event fired at 1225 ms.andThe tea appeared at 4248 ms. - Line 7 subtracts one from the other and prints
After the load event, the tea took another 3023 ms.
So what is this load event? An event is a signal the browser gives the page's code when something happens: a click, a key press, a file arriving. The load event is its signal that the page has loaded its own files: the HTML, the styles, the scripts and the images. Code can listen for it by putting a function in window.onload. That's where the old name "onload" comes from.
Here is what Asha's screen showed during that load, frame by frame:
- Nothing drawn yet
0 ms to 1,232 msA blank screen

At 1,232 msThe header and an empty space. The load event has already fired.

At 4,247 msThe tea and its reviews, at last
By the time the header was drawn, the load event had already fired. By its load time, the page was finished. Asha still had nothing to read but the shop's name and the headings.
Why did it fire so early? The page's script asks the shop's server for the tea and its reviews with fetch(). That's the function a script uses to ask a server for data. The load event didn't wait for those answers. It fired once the HTML, the styles and the script itself were in.
A restaurant times each evening by the moment the last napkin is folded. That's easy to write down, and it happens every night. But you came to eat. On a night when the napkins are done early and your food comes much later, the notebook still says "fast".
- In real life: The last napkin folded
- In web performance: The load event: the page's own files are in
- In real life: Your food arriving
- In web performance: The main content on the screen, like Asha's tea
- In real life: A notebook that says "fast"
- In web performance: A short load time
- In real life: You, at a table with no food
- In web performance: Asha, looking at an empty space
web.dev, Google's site for web developers, describes the same problem. For a long time, speed was measured with the load event. But that moment, it says, "doesn't necessarily correspond with anything the user cares about." Its example is a page like Asha's: one that "loads" quickly and only then fetches its content.
So Ravi needs a different kind of number. A metric is a number that measures one thing about a page, the same way every time, so you can compare. Load time is a metric. A user-centric metric is centred on the user, the person using the page. It measures a moment that person notices: something appears, the main content arrives, a tap gets an answer.
Which moment, then? There isn't just one. Think about waiting for a parcel.
You order a teapot online. Its tracking page doesn't show one time. It shows each stop: dispatched, out for delivery, delivered. Each stop answers a different worry, from "has anything happened?" to "is it here?". Then you open the box, and two more follow: does it work, and is anything broken?
- In real life: "Dispatched": something has started
- In web performance: Is it happening? The first text or image is drawn
- In real life: "Delivered": the teapot is at your door
- In web performance: Is it useful? The main content is on the screen
- In real life: It works when you use it
- In web performance: Is it usable? A tap or a key press gets a quick answer
- In real life: Nothing inside is broken
- In web performance: Is it delightful? The page stays smooth and steady
Lesson 1.1 told the same story as a café visit: a string of moments. In 2017, Philip Walton of Google turned those moments into four questions a visitor asks while a page loads. web.dev still uses them. Read each row across: the question, what the visitor wants to know, and the metric the course uses for it today.
| The question | What the visitor wants to know | The metric that answers it today |
|---|---|---|
| Is it happening? | Has anything started to arrive? | First Contentful Paint (FCP): when the first text or image is drawn. Time to First Byte (TTFB), when the server's first answer arrives, comes before it. |
| Is it useful? | Is the main content there yet? | Largest Contentful Paint (LCP): when the largest text block or image on the screen is drawn. |
| Is it usable? | If I tap or type, does the page answer? | Interaction to Next Paint (INP): how long a tap, click or key press takes to show its result. |
| Is it delightful? | Does it stay smooth and steady? | Cumulative Layout Shift (CLS): a score for how much things jump around. INP counts here too: no lag. |
Each metric in the table measures something the visitor notices. On Asha's page, the first question got its answer at 1,228 ms, when the header was drawn. The second had to wait until 4,248 ms, for the tea. The load event answered neither.
You'll meet these metrics all through the course. Lesson 3.1, Core Web Vitals, takes LCP, INP and CLS one by one, with the numbers to aim for. Lesson 11.3, Paint, LCP, and layout shift APIs, shows how to read FCP, LCP and CLS from JavaScript.
The load event tells you when the page finished its own work. It doesn't tell you what the visitor saw. A user-centric metric measures a moment the visitor notices: something appears, the main content arrives, a tap gets an answer, nothing jumps.
onload and DOMContentLoaded
A page gets two signals while it loads, not one. Picture a new house. The first signal comes when the walls and the roof are up. The second comes when every piece of furniture has been delivered, down to the last chair.
DOMContentLoadedis the walls and the roof. It fires when the browser has read all of the HTML. By then it has built the DOM: its model of the page, with one object for each element.loadis the last chair. It fires when the page's own files have arrived too: stylesheets, scripts and images.
One more word before the table. A deferred script is a script whose tag has the defer attribute. The browser downloads it while it reads the HTML, and runs it once the HTML is read. Each row of the table is one event: when it fires, and what it doesn't wait for. The facts come from MDN, Mozilla's reference site for web developers.
| Event | It fires | What it doesn't wait for |
|---|---|---|
| DOMContentLoaded | When the browser has read all the HTML and run any deferred scripts. | It doesn't wait for images or stylesheets. A deferred script does wait for stylesheets, though, and this event waits for the script. |
| load | When the page's own files have all arrived too: stylesheets, scripts and images. | It doesn't wait for images that load lazily, only when you scroll near them. On Asha's page, it didn't wait for the tea a script fetched. |
Notice what's missing from both rows: the screen. Neither event says anything about what the visitor can see. On Asha's page, both fired together, at 1,225 ms. Its HTML, its styles and its script were all in. The tea came 3,023 ms later.
The load event can be wrong the other way too. The course built a second page to show it. All its text is in the HTML, so nothing waits for a script. But far down the page sits the heavy picture from lesson 1.2, about 580 KB. It's below the fold: further down than the first screen, so you only see it if you scroll. Here are two moments from one recorded load:
// The below-the-fold page: two moments from one recorded load, in msconst everythingDrawn = 1240;const loadEvent = 4760; console.log(`Everything on the screen was drawn at ${everythingDrawn} ms.`);console.log(`The load event fired ${loadEvent - everythingDrawn} ms later.`);- Line 2 is when everything on the first screen was drawn. Line 3 is when the load event fired.
- Line 5 prints
Everything on the screen was drawn at 1240 ms. - Line 6 subtracts one time from the other and prints
The load event fired 3520 ms later.
Here is the same load as a filmstrip. It has only one frame, because the whole first screen was drawn at once:
- Nothing drawn yet
0 ms to 1,238 msA blank screen

At 1,238 msAll the text on the first screen, drawn at once
So what happened in those 3.5 seconds? Step by step:
DOMContentLoadedfired at 611 ms. The screen was still blank. Nothing was drawn untilstyles.csshad arrived, at 1,180 ms.- All the text appeared at 1,240 ms. It was the first text drawn and also the largest, so that moment was both the FCP and the LCP.
loadwaited formint-tea.svg, the picture below the fold. It arrived at 4,413 ms, andloadfired at 4,760 ms. Nobody had scrolled down to see it.
In the house picture, load waited for a piano to be carried up to the attic, while the family was already living downstairs. You can open the page and scroll down to the picture yourself.
Here are both recorded pages on one time line:
WebPageTest, a free tool for testing how pages load, puts it plainly. Content below the fold pushes the load event later, "even if the user-visible content has long-since rendered."
Why does this matter? If Ravi tracks load time, it can change without Asha noticing anything. A smaller picture below the fold could make load fire sooner, and Asha's screen would look exactly the same. The load event is still good for one job: knowing that the page's own files have all arrived. It just doesn't measure what the visitor saw.
The browser writes down the time of both events for every page. Lesson 5.1, The navigation timeline, shows where to read them.
Speed Index and visual progress
If one moment can mislead, why not measure the whole wait? In April 2012, WebPageTest added a metric that tries: Speed Index. It asks how quickly the screen fills in, from the start to the finish. Lower is better.
Picture two people painting the same fence. They finish at the same time. One paints nearly all of it at the very end. The other paints a little at a time. Speed Index counts how much fence stayed bare, and for how long. The second fence was bare for less time, so it scores lower.
Here is the first kind of load in numbers. Nothing appears for 3 seconds, then the whole page at once:
// Nothing on the screen for 3 seconds, then the whole page at onceconst shareMissing = 1; // all of the page is missing...const howLong = 3000; // ...for 3,000 ms console.log(`Speed Index: ${shareMissing * howLong} ms`);- Line 2 is the share of the page still missing. 1 means all of it.
- Line 3 is how long it stayed missing: 3,000 ms.
- Line 5 multiplies the two and prints
Speed Index: 3000 ms.
When a page shows nothing and then everything, its Speed Index is simply the time it took. Now the same 3 seconds, bit by bit:
// The same 3 seconds, but the page fills in bit by bitconst firstSecond = 1000 * 1; // nothing drawn: all of it missingconst secondSecond = 1000 * 0.5; // half drawn: half missingconst thirdSecond = 1000 * 0.2; // 80% drawn: a fifth missing console.log(`Speed Index: ${firstSecond + secondSecond + thirdSecond} ms`);- Line 2 is the first second. Nothing is drawn yet, so all of the page is missing: 1,000 × 1.
- Line 3 is the second second. Half the page is drawn, so half is missing: 1,000 × 0.5.
- Line 4 is the third second. 80% is drawn, so a fifth is missing: 1,000 × 0.2.
- Line 6 adds them up and prints
Speed Index: 1700 ms.
Both loads end at 3 seconds. The second scores 1,300 ms lower, because less of the page was missing along the way. In this picture, each shaded block is one stretch of time, as tall as the share still missing:
A real load has many frames, so WebPageTest does the same sum in a loop. Here it is as a function you can run:
function speedIndex(frames) { let total = 0; for (let i = 1; i < frames.length; i++) { const stretch = frames[i].ms - frames[i - 1].ms; total += stretch * (1 - frames[i - 1].complete); } return total;} const allAtOnce = [{ ms: 0, complete: 0 }, { ms: 3000, complete: 1 }];const bitByBit = [{ ms: 0, complete: 0 }, { ms: 1000, complete: 0.5 }, { ms: 2000, complete: 0.8 }, { ms: 3000, complete: 1 }]; console.log("all at once:", speedIndex(allAtOnce), "ms");console.log("bit by bit:", speedIndex(bitByBit), "ms");- Each frame says when it was shown, in ms, and how complete the screen was, from 0 to 1.
- Line 4 works out how long the previous frame stayed on the screen.
- Line 5 multiplies that by the share still missing in the previous frame, and adds it to the total.
- Lines 10 and 11 are the two loads from above. It prints
all at once: 3000 msandbit by bit: 1700 ms.
How complete is a frame?
WebPageTest records a video of the load. For each frame, it needs a number from 0 to 1. It doesn't try to read the page. It counts colours instead: how much red, green and blue each frame has. The closer a frame's colours are to the finished page's, the more complete it counts. The course worked out its own four recordings the same way, from their filmstrips.
Each row of this table is one recorded page: how complete its first frame was, its LCP, and its Speed Index.
| Page | What it is | First frame complete | LCP | Speed Index |
|---|---|---|---|---|
| Blank | Asha's Tea of the week page. It fetches the tea and reviews from a slow server, and shows nothing in their place. | 57% | 4,248 ms | 2,519 ms |
| Skeleton | The same page, with grey bars where the tea and reviews will go. | 69% | 4,252 ms | 2,164 ms |
| Content first | The same grey bars, but the tea shows as soon as it arrives. | 69% | 2,252 ms | 1,677 ms |
| Below the fold | All its text is in the HTML. A picture of about 580 KB sits below the fold. | 100% | 1,240 ms | 1,238 ms |
- The below-the-fold page drew everything at once, and early. Its Speed Index, 1,238 ms, is almost exactly its LCP.
- The content-first page showed the tea early and the reviews later. Its Speed Index, 1,677 ms, is well below the time it finished.
- The skeleton and blank pages drew the tea at almost the same moment. But the skeleton's grey bars counted as progress. Its Speed Index is 2,164 ms, against 2,519 ms for the blank page.
Those grey bars are lesson 2.2's bread and water. They make the wait feel shorter, and Speed Index rewards them. But the tea came no sooner.
Try it yourself. Pick each recorded load and watch the shaded area: that's its Speed Index.
const frames = recording.filmstrip; // { ms, visual } for each changelet total = 0;let previousMs = 0;let previous = 0;for (const frame of frames) { total += (frame.ms - previousMs) * (1 - previous); previousMs = frame.ms; previous = frame.visual;}showSpeedIndex(total);Blank: the first frame came at 1,232 ms, already 57% complete. The page finished drawing at 4,247 ms. The shaded area adds up to a Speed Index of 2,519 ms, which Lighthouse rates fast on a phone. Its LCP was 4,248 ms, and its load event fired at 1,225 ms.
Line 6 of the panel's code is the same sum as line 5 of the function above, done frame by frame.
One number hides the details
Lighthouse is Google's page-testing tool. You can run it from DevTools, the developer tools built into Chrome. It still reports Speed Index, worked out its own way. On phones, it rates a Speed Index fast up to 3.4 seconds, and slow over 5.8 seconds.
By that scale, the blank page's Speed Index of 2,519 ms is fast. Yet its LCP, 4,248 ms, is far past the 2.5 seconds web.dev sets as the goal. How can both be true? The header, the heading and the footer were drawn early. They made the first frame 57% complete by the colour count. The one part that mattered, the tea, came last.
A school report shows one overall grade: a B. It looks fine. But it hides an A in art and a failed maths test. To help, a teacher needs the grade for each subject.
- In real life: The one overall grade
- In web performance: One number for the whole load, like Speed Index or load time
- In real life: The A in art
- In web performance: The header and the footer, drawn early
- In real life: The failed maths test
- In web performance: The tea, drawn late
- In real life: A grade for each subject
- In web performance: A metric for each question: FCP, LCP, INP and CLS
web.dev says the same: "no single metric is sufficient to capture all the performance characteristics of a page." That's why each of the four questions gets its own metric. WebPageTest's own notes add two more limits to Speed Index:
- It needs a video of the load. So it works only in a lab test: one you run on a machine you control, not on your visitors' phones.
- It compares every frame with the last one. A video playing on the page, or a moving slideshow, can confuse it.
Lesson 3.2, The supporting metrics, says when Speed Index is still worth a look.
Retired metrics: FMP, TTI and FID
Speed Index wasn't the only try. In 2017, Google answered the four questions with a set of metrics. Two of those metrics have since been replaced. This picture shows the 2017 answers on top, and today's below:
Three metrics from those years have been retired: First Meaningful Paint, Time to Interactive and First Input Delay. Before looking at each one, it helps to ask what a good metric needs.
A referee's stopwatch starts at kick-off and stops at the final whistle. Everyone knows both moments, and everyone cares about them. A watch that stopped "when the crowd goes home" would be no use: nobody can say when that is, and it isn't part of the match.
- In real life: Kick-off
- In web performance: A clear start, like the start of the load or a tap
- In real life: The final whistle
- In web performance: A clear end the visitor sees, like the tea being drawn
- In real life: "When the crowd goes home"
- In web performance: A fuzzy end that moves by seconds, like TTI's quiet window
So a good metric needs three things:
- a clear start, and a clear end that the visitor cares about;
- the same result each time the page loads the same way;
- a definition every browser can measure.
Each retired metric missed at least one of these. The table about them uses a few new words:
- Above the fold is the first screen: the part you see without scrolling.
- The main thread is the one worker that runs the page's JavaScript and handles taps and clicks. While it's busy, a tap has to wait.
- A long task is a piece of work that keeps the main thread busy for more than 50 ms.
- To deprecate a metric is to mark it as on its way out, before it's removed.
- Lighthouse's score mixes several metrics. A metric's weight is its share of that score.
- Total Blocking Time (TBT) adds up how long the main thread was too busy to answer a tap while the page loaded.
Each row is one retired metric: what it measured, why it went, and what replaced it.
| Metric | What it measured | Why it went | Replaced by |
|---|---|---|---|
| First Meaningful Paint (FMP) | The paint after the biggest change to the layout above the fold. | Its time jumped around from load to load, and only Chrome could measure it. Lighthouse 6 deprecated it in 2020, and Lighthouse 13 removed it. | Largest Contentful Paint (LCP). |
| Time to Interactive (TTI) | The end of the last long task before 5 quiet seconds. | One small, late task or network request could move it by seconds. Lighthouse 6 cut its weight from 33% to 15%, and Lighthouse 10 removed it. | Total Blocking Time (TBT) in a lab test, and INP for real visitors. |
| First Input Delay (FID) | How long the first tap or click waited before its code could start. | It missed the click's own code, the drawing after it, and every later tap. INP replaced it on March 12, 2024, and support for FID ended on September 9, 2024. | Interaction to Next Paint (INP). |
First Meaningful Paint: a finish line that moved
FMP tried to answer "Is it useful?". It looked for the paint after the biggest change to the layout above the fold. But its time jumped around from one load to the next. And it was built on Chrome's own drawing code, so no other browser could measure it. The Lighthouse 6.0 release notes say it had "no viable path to standardization". LCP answers the same question today. Lesson 17.1, How LCP works, shows how the browser picks the largest element.
Time to Interactive: one late task moves it
TTI tried to answer "Is it usable?". It started at the first contentful paint. Then it looked ahead for a quiet window: 5 seconds with no long tasks. TTI was the end of the last long task before that window. The real metric also wanted no more than two network requests in the window. This example leaves that part out:
function timeToInteractive(fcp, longTasks) { let tti = fcp; for (const task of longTasks) { if (task.start - tti >= 5000) break; tti = task.end; } return tti;} const tasks = [{ start: 1200, end: 1400 }, { start: 2000, end: 2100 }];console.log("TTI:", timeToInteractive(1000, tasks), "ms");console.log("one more task:", timeToInteractive(1000, [...tasks, { start: 6900, end: 6960 }]), "ms");- Line 2 starts TTI at the first contentful paint,
fcp. - Line 4 checks the gap since TTI. A gap of 5,000 ms or more is the quiet window, so the search stops there.
- Line 5 runs otherwise. TTI moves to the end of this task.
- Line 10 lists two long tasks. The last one ends at 2,100 ms, so line 11 prints
TTI: 2100 ms. - Line 12 adds one more task, 60 ms long, starting 4,800 ms after the last one ended. It prints
one more task: 6960 ms.
One small task moved TTI by almost 5 seconds. web.dev says TTI proved "overly sensitive to outlier network requests and long tasks". That's the fuzzy end of the referee's watch. Lighthouse 10 removed it.
First Input Delay: it stopped the clock too early
FID tried to answer "Is it usable?" for real visitors. To see what it missed, split one click into three parts:
- Input delay: the wait before the click's code can start, because the main thread is busy.
- Processing: the click's own code running. These functions are its event handlers: code that runs when an event happens.
- Presentation delay: drawing the result on the screen, in the next frame.
const click = { inputDelay: 12, processing: 280, presentation: 16 }; const fid = click.inputDelay;const wholeClick = click.inputDelay + click.processing + click.presentation; console.log(`FID saw ${fid} ms.`);console.log(`The visitor waited ${wholeClick} ms.`);- Line 1 is one click: a 12 ms wait, 280 ms of its own code, and 16 ms to draw.
- Line 3 is FID: the wait, and nothing else.
- Line 4 adds all three parts. That's the time from the click to its result on the screen.
- Lines 6 and 7 print
FID saw 12 ms.andThe visitor waited 308 ms.
FID stopped its clock when the click's code began. The slow part came after. FID also looked only at the first click of a visit. Its replacement, Interaction to Next Paint (INP), times every click, tap and key press, right up to the next frame. With fewer than 50 of them on a page, it reports the slowest.
const clicks = [ { name: "first click", inputDelay: 12, processing: 280, presentation: 16 }, { name: "later click", inputDelay: 90, processing: 40, presentation: 16 },]; const fid = clicks[0].inputDelay;const inp = Math.max(...clicks.map((click) => click.inputDelay + click.processing + click.presentation)); console.log("FID:", fid, "ms");console.log("INP:", inp, "ms");- Lines 1 to 4 are two clicks, each split into the same three parts.
- Line 6 is FID: the first click's wait.
- Line 7 is INP: each click's whole time, keeping the slowest.
- It prints
FID: 12 msandINP: 308 ms.
By FID, this page answered quickly. By INP, one click took 308 ms, more than the 200 ms web.dev counts as good. On March 12, 2024, INP replaced FID as a Core Web Vital, one of the few metrics web.dev asks every site to measure.
Now feel the difference. The Add to cart button below runs its code for 200 ms. Click it once. Then press Keep the page busy, and click Add to cart again straight away:
new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name !== "click") continue; const inputDelay = entry.processingStart - entry.startTime; const processing = entry.processingEnd - entry.processingStart; showClick({ inputDelay, processing, duration: entry.duration }); }}).observe({ type: "event", durationThreshold: 16 });No clicks timed yet.
Click Add to cart. Its code keeps the main thread busy for 200 ms. Then press Keep the page busy, and click Add to cart again straight away, while a 600 ms task runs.
In the panel's code, line 4 works out a click's wait: what FID measured. Line 5 works out how long its code ran. Line 6 passes on the whole time to the next frame: what INP measures.
How a metric becomes a standard
FMP shows the first thing a metric needs to last: a definition every browser can follow, not just Chrome.
A café's best cake used to depend on one chef's feel for it. When the café opened a second branch, the cake came out different there. So the chef wrote the recipe down, step by step, and now every kitchen bakes the same cake.
- In real life: The chef's feel for the cake
- In web performance: FMP: built on Chrome's own drawing code
- In real life: The written recipe
- In web performance: A W3C standard, like Largest Contentful Paint
- In real life: Every branch bakes the same cake
- In web performance: Every major browser engine measures LCP the same way
For the web, the recipes are written at the W3C, the World Wide Web Consortium. It's where browser makers and others agree on how web features work. Its Web Performance Working Group writes the standards behind today's metrics, such as Paint Timing, Largest Contentful Paint and Event Timing. web.dev says the Chrome team worked with that group on the new metrics.
The second thing a metric needs is time. Sites need time to measure a new metric and fix what it finds. So web.dev gives every candidate Core Web Vital three stages:
- Experimental: it's being tested, and it can still change.
- Pending: it's nearly ready. It stays here for at least six months, so sites can adapt.
- Stable: it can become a Core Web Vital. From then on, it changes at most once a year, with notice.
INP went through every stage. Each row of the next table is one step on its road. Its last row talks about browser engines. A browser engine is the part of a browser that reads and draws pages. Chrome, Firefox and Safari each use a different one.
| When | What happened |
|---|---|
| May 2022 | INP arrives as an experimental metric. |
| 2023 | INP becomes a pending metric, with the plan to retire FID. |
| March 12, 2024 | INP becomes a stable Core Web Vital, and FID is deprecated. |
| September 9, 2024 | Support for FID ends. |
| December 12, 2025 | Safari 26.2 ships. Every major browser engine can now measure LCP and INP. |
The last row is the recipe reaching every kitchen. For years, LCP and INP could only be measured in Chromium browsers, such as Chrome and Edge. Then they joined Interop 2025, a project in which the browser makers worked on the same features together. With Safari 26.2, on December 12, 2025, every major browser engine could measure both. CLS wasn't part of that project. When web.dev announced the news, only Chromium browsers could measure CLS.
Why does it matter? A metric that every browser measures the same way tells you about all your visitors, not only the ones using Chrome.
Which question does it answer?
Each card names something a metric has measured, old or new. Sort it by the question it answers. For each one, ask: what would the visitor notice?
- Time to First Byte
- First Contentful Paint
- Largest Contentful Paint
- First Meaningful Paint, in 2017
- Total Blocking Time
- Time to Interactive, in 2017
- Cumulative Layout Shift
- Long tasks during an animation
Sort each metric by the question it answers.
Common mix-ups
- "Load time is how long the page takes." No. It's when the page's own files are in. On Asha's page, load fired seconds before the tea. On the below-the-fold page, it fired seconds after the text.
- "DOMContentLoaded means the page is visible." No. On the below-the-fold page, it fired while the screen was still blank.
- "Speed Index measures content." No. It measures how close the screen's colours are to the finished page's. The skeleton's grey bars lowered it, and they aren't content.
- "A good FID meant the page answered quickly." No. FID saw only the first click's wait. A click whose code ran for 280 ms could still have a 12 ms FID.
- "TTI is the moment the page can be used." Only roughly. One small, late task could move it by seconds. That's why Lighthouse removed it.
- "Google can change the Core Web Vitals any time." No. A new metric is experimental, then pending for at least six months. Stable ones change at most once a year.
Practice exercises
Answer first, then use the hints.
This load shows nothing for 2 seconds, then 90% of the page at once. What does the last line print?
function speedIndex(frames) {
let total = 0;
for (let i = 1; i < frames.length; i++) {
const stretch = frames[i].ms - frames[i - 1].ms;
total += stretch * (1 - frames[i - 1].complete);
}
return total;
}
const allAtOnce = [{ ms: 0, complete: 0 }, { ms: 3000, complete: 1 }];
const bitByBit = [{ ms: 0, complete: 0 }, { ms: 2000, complete: 0.9 }, { ms: 3000, complete: 1 }];
console.log("all at once:", speedIndex(allAtOnce), "ms");
console.log("bit by bit:", speedIndex(bitByBit), "ms");bit by bit: 2100 ms. That's 2,000 ms with all of the page missing, plus 1,000 ms with a tenth missing: 2,000 + 100. It's worse than the 1,700 ms of the first bit-by-bit load, because nothing showed for 2 whole seconds.
What does this print?
function timeToInteractive(fcp, longTasks) {
let tti = fcp;
for (const task of longTasks) {
if (task.start - tti >= 5000) break;
tti = task.end;
}
return tti;
}
console.log(timeToInteractive(800, [{ start: 900, end: 1000 }, { start: 5500, end: 5560 }]));5560. The gap is 4,500 ms. That's short of 5 quiet seconds, so TTI moves to the end of the 60 ms task, at 5,560 ms.
What does the first line print?
const clicks = [
{ name: "first click", inputDelay: 150, processing: 20, presentation: 16 },
{ name: "later click", inputDelay: 90, processing: 40, presentation: 16 },
];
const fid = clicks[0].inputDelay;
const inp = Math.max(...clicks.map((click) => click.inputDelay + click.processing + click.presentation));
console.log("FID:", fid, "ms");
console.log("INP:", inp, "ms");FID: 150 ms. The second line is INP: 186 ms, under the 200 ms web.dev counts as good. FID can look slow while INP looks fine, and the other way round, because they measure different things.
In the Speed Index playground, pick Content first. When did that page finish drawing, in ms?
4,249 ms, about when the blank page finished too. Content first didn't finish any sooner. Its Speed Index is lower because the tea came early, so less of the page was missing along the way.
Asha's blank page was recorded in Chrome. Download its trace and open it in DevTools' Performance panel. Which file arrives last?
reviews.json, at about 4.2 seconds. That's long after the load event's marker. The page's script waits for it before it draws the tea, so the LCP comes just after it.
Open Lighthouse's performance scoring page. In the newest weights it lists, how much of the Performance score is Speed Index?
10%. Largest Contentful Paint carries 25%, and Total Blocking Time 30%. Lighthouse still shows Speed Index, but most of the score comes from metrics that each answer one question.
Check your understanding
For each question, ask: does this measure a moment the visitor notices, or the page's own work?
Question 1 of 7What does the second line of the one-click example print?
Read the code, then predictconst click = { inputDelay: 12, processing: 280, presentation: 16 }; const fid = click.inputDelay; const wholeClick = click.inputDelay + click.processing + click.presentation; console.log(`FID saw ${fid} ms.`); console.log(`The visitor waited ${wholeClick} ms.`);Choose an answer to see the explanation.
Question 2 of 7On Asha's recorded blank page, when did the load event fire, compared with the tea appearing?
Choose an answer to see the explanation.
Question 3 of 7Why did the recorded below-the-fold page's load event come so late?
Choose an answer to see the explanation.
Question 4 of 7What does the last line of the Speed Index function example print?
Read the code, then predictfunction speedIndex(frames) { let total = 0; for (let i = 1; i < frames.length; i++) { const stretch = frames[i].ms - frames[i - 1].ms; total += stretch * (1 - frames[i - 1].complete); } return total; } const allAtOnce = [{ ms: 0, complete: 0 }, { ms: 3000, complete: 1 }]; const bitByBit = [{ ms: 0, complete: 0 }, { ms: 1000, complete: 0.5 }, { ms: 2000, complete: 0.8 }, { ms: 3000, complete: 1 }]; console.log("all at once:", speedIndex(allAtOnce), "ms"); console.log("bit by bit:", speedIndex(bitByBit), "ms");Choose an answer to see the explanation.
Question 5 of 7In the recordings, the skeleton page had a lower Speed Index than the blank page, but the same LCP. Why?
Choose an answer to see the explanation.
Question 6 of 7Why was First Input Delay replaced by Interaction to Next Paint?
Choose an answer to see the explanation.
Question 7 of 7What does a metric go through before it becomes a stable Core Web Vital?
Choose an answer to see the explanation.
Key takeaways
- The load event marks when the page's own files are in, not when the visitor sees anything. In the recordings, it came 3,023 ms before one page's content and 3,520 ms after the other's.
- A user-centric metric measures a moment the visitor notices. Four questions name those moments: is it happening, is it useful, is it usable, is it delightful.
- Speed Index adds up how much of the screen is missing over time. It works only in a lab, and it counts placeholders as progress.
- One number hides which part went wrong. That's why each question gets its own metric.
- FMP, TTI and FID were retired: too jumpy, too easily moved, or stopped too early. LCP, TBT and INP replaced them.
- A metric lasts when every browser can measure it the same way. Core Web Vitals change slowly, and with notice.
Remember the one-liner.
Measure the moments people notice: something appears, the content arrives, a tap gets an answer, nothing jumps.
Sources
The facts on this page come from these articles, docs and specifications:
- User-centric performance metrics, Philip Walton, web.dev.
- User-centric performance metrics (2017 version, archived), Philip Walton, Google Developers.
- Window: load event, MDN.
- Document: DOMContentLoaded event, MDN.
- <script>: the defer attribute, MDN.
- Lazy loading, MDN.
- Speed Index, WebPageTest documentation.
- Introduction to Lighthouse, Chrome for Developers.
- Speed Index, Lighthouse documentation.
- Lighthouse performance scoring, Chrome for Developers.
- Lighthouse 6.0, Chrome for Developers.
- First Meaningful Paint, Lighthouse documentation.
- Time to Interactive (TTI), web.dev.
- What's new in Lighthouse 10, Chrome for Developers.
- First Input Delay (FID), web.dev.
- Interaction to Next Paint (INP), web.dev.
- Interaction to Next Paint becomes a Core Web Vital on March 12, web.dev.
- Web Performance Working Group, W3C.
- Web Vitals: the Core Web Vitals lifecycle, web.dev.
- LCP and INP are now Baseline Newly available, web.dev.
Coming next: Core Web Vitals. It takes LCP, INP and CLS one by one: what each measures, the numbers to aim for, and how a site passes.