completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

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.

By the end, you can
  • 01
    Ask the four questionsSay which question a metric answers: is it happening, is it useful, is it usable, or is it delightful.
  • 02
    Tell 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.
  • 03
    Explain 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:

Two moments from one loadPop out in the code editor (opens in a new tab)JavaScript
// 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 load event fired. Line 3 is when the tea appeared on the screen.
  • Lines 5 and 6 print them: The load event fired at 1225 ms. and The 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:

  1. Nothing drawn yet

    0 ms to 1,232 msA blank screen

  2. The header, Tea of the week, an empty space, the Reviews heading and the footer.

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

  3. Mint tea, its description and price, and three reviews.

    At 4,247 msThe tea and its reviews, at last

Asha's Tea of the week page, the blank version, recorded in Chrome 156.

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.

Real-life analogyJudging a restaurant by its last folded napkin

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.

Real-life analogyA parcel's tracking page

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 four questions
The questionWhat the visitor wants to knowThe 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 main idea

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.

  • DOMContentLoaded is 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.
  • load is 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.

Two load events
EventIt firesWhat it doesn't wait for
DOMContentLoadedWhen 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.
loadWhen 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:

Everything drawn, then a long wait for loadPop out in the code editor (opens in a new tab)JavaScript
// 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:

  1. Nothing drawn yet

    0 ms to 1,238 msA blank screen

  2. The header, How we choose our teas, a long introduction, and the Tasting, Packing and Delivery sections.

    At 1,238 msAll the text on the first screen, drawn at once

The below-the-fold page, recorded in Chrome 156.

So what happened in those 3.5 seconds? Step by step:

  • DOMContentLoaded fired at 611 ms. The screen was still blank. Nothing was drawn until styles.css had 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.
  • load waited for mint-tea.svg, the picture below the fold. It arrived at 4,413 ms, and load fired 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:

Two recorded loads on a time line from 0 to 5 seconds. The blank page: DOMContentLoaded at 1,225 ms, load at 1,225 ms, FCP at 1,228 ms, LCP at 4,248 ms. The below-the-fold page: DOMContentLoaded at 611 ms, FCP at 1,240 ms, LCP at 1,240 ms, load at 4,760 ms.BlankDOMContentLoaded, loadFCPLCPBelow the foldDOMContentLoadedFCP, LCPload0 s5 s
The load events sit above each line, and the paints below it. On the blank page, load fired long before the tea. On the below-the-fold page, it fired long after the text.

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:

All at once, after 3 secondsPop out in the code editor (opens in a new tab)JavaScript
// 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:

Bit by bit, over the same 3 secondsPop out in the code editor (opens in a new tab)JavaScript
// 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:

Two loads that both finish at 3 seconds. All at once: the whole page is missing for 3 seconds, a Speed Index of 3,000 milliseconds. Bit by bit: all of it is missing for the first second, half for the second, a fifth for the third, a Speed Index of 1,700 milliseconds. Each shaded block is a stretch of time times the share still missing.All at once3,000 msBit by bit1,700 ms0 s1 s2 s3 s
Speed Index is the shaded area: the share of the page still missing, added up over time. Both loads end at 3 seconds. The one that shows its parts sooner has the smaller area.

A real load has many frames, so WebPageTest does the same sum in a loop. Here it is as a function you can run:

Speed Index for any list of framesPop out in the code editor (opens in a new tab)JavaScript
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 ms and bit 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.

Speed Index of the recorded pages
PageWhat it isFirst frame completeLCPSpeed Index
BlankAsha'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 ms2,519 ms
SkeletonThe same page, with grey bars where the tea and reviews will go.69%4,252 ms2,164 ms
Content firstThe same grey bars, but the tea shows as soon as it arrives.69%2,252 ms1,677 ms
Below the foldAll its text is in the HTML. A picture of about 580 KB sits below the fold.100%1,240 ms1,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.

Playground: Speed Index from a recorded load
What this panel computes, shortenedJavaScript
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);
Visual progressSpeed Index 2,519 ms
LCPload0 s5 s100%0%
Try it yourself
Its Speed Index

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.

Each load was recorded in Chrome. Each frame's progress was worked out from its colours, the way WebPageTest describes. The numbers come from those recordings, not from your browser.

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.

Real-life analogyOne grade on a school report

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:

Four questions along a page load, left to right: Is it happening? Is it useful? Is it usable? Is it delightful? Under each, the metric that answered it in 2017, and the ones that answer it now. Is it happening: FCP in 2017, TTFB and FCP now. Is it useful: FMP, now LCP. Is it usable: TTI, now TBT and INP. Is it delightful: no long tasks, now INP and CLS.Is it happening?2017FCPnowTTFB, FCPIs it useful?2017FMPnowLCPIs it usable?2017TTInowTBT, INPIs it delightful?2017no long tasksnowINP, CLS
The four questions haven't changed since 2017. Two of the 2017 metrics have been replaced: LCP took over from FMP, and TBT and INP from TTI.

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.

Real-life analogyA referee's stopwatch

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.

Retired metrics
MetricWhat it measuredWhy it wentReplaced 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:

Time to Interactive, simplifiedPop out in the code editor (opens in a new tab)JavaScript
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.
One click, three partsPop out in the code editor (opens in a new tab)JavaScript
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. and The 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.

FID saw one click, INP sees them allPop out in the code editor (opens in a new tab)JavaScript
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 ms and INP: 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:

Playground: what FID saw, and what INP sees
What this panel runs, shortenedJavaScript
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 });
Cart: 00 clicks timed

No clicks timed yet.

Try it yourself
FID and INP

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.

Real times from your browser, which reports them rounded to 8 ms. The click's code and the busy task are real loops that run for 200 and 600 ms.

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.

Real-life analogyA recipe written down for every kitchen

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.
Three stages from left to right. Experimental: tested, and it can still change; INP entered it in May 2022. Pending: at least six months' notice; INP in 2023. Stable: changes at most once a year; INP from March 12, 2024.Experimentaltested, can still changeINP: May 2022Pendingat least six months' noticeINP: 2023Stablechanges at most once a yearINP: March 12, 2024
A new metric moves from left to right. Under each stage is the date INP reached it.

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.

INP's road
WhenWhat happened
May 2022INP arrives as an experimental metric.
2023INP becomes a pending metric, with the plan to retire FID.
March 12, 2024INP becomes a stable Core Web Vital, and FID is deprecated.
September 9, 2024Support for FID ends.
December 12, 2025Safari 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?

Which question does it answer?
  • 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
Try it yourself
0 of 8 correct

Sort each metric by the question it answers.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.

Exercise 1 · Warm-upMost of the page at 2 seconds

This load shows nothing for 2 seconds, then 90% of the page at once. What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
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");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upA late task

    What does this print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    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 }]));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWhen the first click is the slow one

      What does the first line print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      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");

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWhen did Content first finish?

        In the Speed Index playground, pick Content first. When did that page finish drawing, in ms?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeThe last file to arrive

          Asha's blank page was recorded in Chrome. Download its trace and open it in DevTools' Performance panel. Which file arrives last?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeSpeed Index's share of the score

            Open Lighthouse's performance scoring page. In the newest weights it lists, how much of the Performance score is Speed Index?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask: does this measure a moment the visitor notices, or the page's own work?

              The user-centric metrics quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the second line of the one-click example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                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.`);

                Choose an answer to see the explanation.

              2. 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.

              3. Question 3 of 7Why did the recorded below-the-fold page's load event come so late?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does the last line of the Speed Index function example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                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");

                Choose an answer to see the explanation.

              5. 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.

              6. Question 6 of 7Why was First Input Delay replaced by Interaction to Next Paint?

                Choose an answer to see the explanation.

              7. 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:

              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.

              CompleteFrontend Clear concepts. Working examples.