completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

INP fixes from real sites

How real teams found and fixed slow interactions: heavy search handlers, third-party scripts on every click and over-rendering, with their own numbers.

By the end, you can
  • 01
    Spot the three usual causesTell heavy work on every key, someone else's code on every click, and drawing more than changed apart in a team's write-up.
  • 02
    Match each cause to its fixPick debouncing or yielding, a third party that waits, or rendering less, and find the lesson that teaches it.
  • 03
    Fix the slowest interaction firstUse field data to find the interaction that sets INP, and measure again after each change.

Did real teams find the same slow taps?

Ravi has spent the last few lessons on the Leaf and Kettle's tea list. One slow tap at a time, he found the cause and fixed it. A search that worked hard on every key. A click that did four jobs at once. A list that drew 3,000 rows again.

Now he wonders. Were these his own mistakes, or do real teams meet the same ones? And when they fixed them, which fixes paid off most?

Many teams have written down what they found. They measured their real visitors, found the slow taps, fixed them, and measured again. Here is where four of them started, and where they ended:

Four teams' INP before and after their fixes, as each team reported it. QuintoAndar: 1,006 ms before, 216 ms after. Disney+ Hotstar: 675 ms before, 272 ms after. Poptin's stores: 280 ms before, 120 ms after. Preply search: about 250 ms before, 175 ms after. A dashed line marks 200 ms, the top of a good INP.beforeafterQuintoAndar1,006 ms216 msDisney+ Hotstar675 ms272 msPoptin's stores280 ms120 msPreply searchabout 250 ms175 ms0200 ms500 ms1,000 ms
Each team's INP before and after its fixes, from its own write-up. Each measured its own visitors its own way, so compare a team only with itself. The dashed line is 200 ms, the top of a good INP.

Each pair of bars is one team's INP, Interaction to Next Paint. Lesson 26.1 showed what it measures. For each click, tap or key press, it times the wait from the press to the next frame: the next picture the browser draws on the screen. A visit's INP is roughly its slowest one. A good INP is 200 ms or less. A millisecond (ms) is a thousandth of a second.

Every team started above the 200 ms line. Two ended under it, and the other two came much closer. Why it matters: slow taps are not bad luck on one small site. Big, busy sites had them too, and fixed them.

Ravi read every write-up. The same three causes came up again and again:

  • Heavy work on every key or filter: a search or a filter that does all its work, every time.
  • Someone else's code on every click: a cookie banner, a tracker or a widget that works inside your clicks.
  • Drawing more than changed: one small change, like one more tea in the cart, that redraws the whole page.
Real-life analogyA bike shop's repair book

A bike shop writes every repair in a book. After a year, the owner reads it through. Most entries are the same three faults: flat tyres, loose chains and worn brakes. A new mechanic doesn't need a thousand stories. She needs to spot those three, and know the repair for each.

In real life: One entry in the repair book
In JavaScript: One team's write-up
In real life: Flat tyres, loose chains, worn brakes
In JavaScript: The three causes of slow interactions
In real life: The usual repair for each fault
In JavaScript: The usual fix for each cause
The main idea

Real slow interactions come from a few causes, and the fixes repeat. Find the slowest interaction first. Then show the change, and do the rest later, less often, or not at all. Each fix here links to the lesson that teaches it. This lesson puts them side by side.

Where the numbers come from

  • The teams' numbers come from each team's own write-up, as they measured them. The Sources list at the end links to every one.
  • Each team measured in its own way. Some timed their own visitors. Some used Chrome's data from real users. So compare a team with itself, not with another team.
  • One test page: Ravi's tea list with all three causes at once, then with the fixes added one by one. The course recorded it in Chrome 157.

Ravi's tea list with all three problems

To see the three causes side by side, Ravi built one page with all of them. It's the tea list from lesson 28.2, with 3,000 teas. Asha does what a shopper does. She clicks Add on Mint tea. Then she clicks the search box and types chai, a key every 120 ms. That's six interactions: two clicks and four key presses.

First, the shop's search. It runs on every key press:

shop.js: search on every keyJavaScript
function search(words) {  work(35_000_000); // scores every tea against the words  let found = 0;  for (const row of teas.children) {    row.hidden = !row.dataset.name.toLowerCase().includes(words);    if (!row.hidden) found += 1;  }  document.querySelector("#about").textContent = "Teas found: " + found;}box.addEventListener("input", () => search(box.value.toLowerCase()));
  • Line 2 stands for the slow part: scoring every tea against the words. work() is the course's stand-in loop. It takes the same number of steps on any computer, so a slower phone takes longer.
  • Lines 4 to 7 hide each row that doesn't match. Line 8 says how many teas were found.
  • Line 10 runs the search on every input event: once per key.

Second, a tracker. It's a third party: code from another company that the page loads, like analytics, ads or a cookie banner. This one notes every click on the page:

tracker.js: work inside every clickJavaScript
document.addEventListener("click", function track(event) {  const report = { on: event.target.tagName, at: event.timeStamp };  work(30_000_000); // reads the page around the click  navigator.sendBeacon("/collect", JSON.stringify(report));});
  • Line 1 listens for clicks on the whole document. Every click on the page reaches it.
  • Line 2 notes what was clicked, and when.
  • Line 3 stands for the tracker's work: reading the page around the click.
  • Line 4 sends the report with sendBeacon, which doesn't wait for an answer.

Third, Add to cart. It adds the tea, then draws the whole list again from its data:

shop.js: Add draws everythingJavaScript
function render() {  document.querySelector("#cart").textContent = "Cart: " + cart.length;  const words = box.value.toLowerCase();  teas.innerHTML = all.map((tea) => rowHtml(tea, words)).join("");}teas.addEventListener("click", function addToCart(event) {  if (!event.target.matches(".add")) return;  cart.push(event.target.closest("li").dataset.tea);  render(); // the badge and all 3,000 rows, drawn again});
  • Lines 1 to 5 are render(). Line 2 writes the cart badge.
  • Line 4 builds the HTML of all 3,000 rows with rowHtml(), and puts it in the page. A tea in the cart says Added.
  • Line 8 adds the tea to the cart. Line 9 calls render().

Here is what Asha's six interactions cost, from Chrome's trace:

Six interactions on the page as it wasPop out in the code editor (opens in a new tab)JavaScript
// Asha's six interactions on the page as it was, in ms, from Chrome's trace.const visit = [  { name: "Add", took: 452.8, waited: 2.1, handlers: 214.7, after: 235.9 },  { name: "Search box", took: 142.3, waited: 0.3, handlers: 134.0, after: 8.0 },  { name: "c", took: 347.4, waited: 0.5, handlers: 337.6, after: 9.3 },  { name: "h", took: 227.5, waited: 63.7, handlers: 154.5, after: 9.3 },  { name: "a", took: 419.5, waited: 105.3, handlers: 309.1, after: 5.0 },  { name: "i", took: 297.0, waited: 138.1, handlers: 153.9, after: 5.0 },];for (const step of visit) {  console.log(`${step.name}: took ${step.took.toFixed(1)}, waited ${step.waited.toFixed(1)}, handlers ${step.handlers.toFixed(1)}, after ${step.after.toFixed(1)}`);}
  • Line 1 says where the numbers come from. A trace is Chrome's detailed record of everything it did.
  • Lines 3 to 8 hold one interaction each. took is its time as INP counts it, from the press to the next frame.
  • The other three are the parts lesson 26.2 named. waited is the input delay, before its handlers started. handlers is the processing. after is the presentation delay: the browser's own work until the frame reached the screen.
  • Line 11 prints one line each. The first is Add: took 452.8, waited 2.1, handlers 214.7, after 235.9. The slowest key is a: took 419.5, waited 105.3, handlers 309.1, after 5.0.

Each cause shows. The Add click spent 214.7 ms in handlers, with the tracker inside them, and 235.9 ms on 3,000 new rows. The click in the search box does nothing for the shop, yet took 142.3 ms: the tracker again. And each key waited behind the search for the key before it.

Here is what Asha saw. These are real frames from the recording, the pictures Chrome drew on her screen. The top row is the page as it was. The bottom row has the three fixes this lesson walks through.

Real frames of Asha's screen as she clicks Add on Mint tea, then types chai. As it was: Cart: 1 showed 455.0 ms after her click. After her c, the first new frame came at 349.5 ms and showed ch: 2 teas; the results for chai came at 659.3 ms. All fixed: Cart: 1 showed 73.8 ms after her click. After her c, the first new frame came at 16.4 ms and showed c, no search yet; the results for chai came at 797.7 ms.Clicking Add on Mint teaTyping chai, after the cAs it washer clickThe tea list, with Cart: 0 and an Add button on Mint teaCart: 0+455.0 msCart: 1, and Mint tea's button says AddedCart: 1, Added+349.5 msThe search box says ch. Teas found: 2.ch: 2 teas+659.3 msThe search box says chai. Teas found: 1.chai: 1 teaAll fixedher clickThe tea list, with Cart: 0 and an Add button on Mint teaCart: 0+73.8 msCart: 1, and Mint tea's button says AddedCart: 1, Added+16.4 msThe search box says c. The list hasn't changed yet.c, no search yet+797.7 msThe search box says chai. Teas found: 1.chai: 1 tea
Real frames from the course's recording on Asha's phone. On the left, times are after her click on Add; on the right, after her first key, c. With the three fixes, Cart: 1 and each letter came at once, and the results came a little later.

On the page as it was, Cart: 1 came 455.0 ms after her click. After her c, nothing changed for 349.5 ms. Then two letters, ch, came at once. With the fixes, Cart: 1 came after 73.8 ms, and the c after 16.4 ms. The results came a little later, at 797.7 ms, because the search now waits for a pause. Why it matters: three ordinary problems made a simple visit feel stuck at every step.

How the course recorded it

  • Four versions of the page: as it was, then with fix 1, fixes 1 and 2, and all three. Each keeps the fixes before it.
  • Asha's phone: this computer in Chrome 157, with its processor, the CPU, slowed 4 times, as in lessons 28.1 to 28.3. Her window was 412 by 480 pixels.
  • Real clicks and keys: sent through Chrome's DevTools protocol, the way programs drive Chrome. Chrome passes mouse clicks and key presses straight to the page.
  • Three visits each, taking turns. The lesson quotes the middle visit, by its slowest interaction.

Heavy work on every key or filter

The first cause showed up in search boxes and filters. A filter is a control that narrows a list, like a date or a price. Their handlers do real work: they search, sort and draw the results. On a fast laptop that feels fine. On a slow phone, each key waits for the work of the key before.

Preply, a site for finding tutors, saw it in its search page's subject box. On slow devices, most key presses there took over 300 ms. The team debounced the page's three input boxes. Debouncing means waiting until the keys stop for a moment, then running the update once. Typing in the subject box went from 632 ms to 72 ms.

QuintoAndar, a housing site in Brazil, found parts of its property search where one interaction took 4 seconds at the 75th percentile. That means three visitors in four were faster, and one in four waited that long or longer. Their fix for a filter click had three steps:

  1. Show a spinner, so the click has a visible answer.
  2. Yield: stop for a moment, so the browser can draw the spinner and handle input. Lesson 27.2 showed how.
  3. Then load the filter's data and open its window.
Real-life analogySaying "one moment, please" first

You ring a hotel to change a booking. The receptionist says "One moment, please", then looks it up. The lookup takes as long as before, but you know you were heard. If she said nothing for a minute, you'd think the line had dropped.

In real life: "One moment, please"
In JavaScript: The spinner, drawn in the next frame
In real life: Putting the phone down to look it up
In JavaScript: Yielding, then running the slow part
In real life: Silence on the line
In JavaScript: A frozen screen: no new frame

Trendyol, a large online shop in Turkey, found one task of 700 to 900 ms on its product list. It started inside an Intersection Observer callback, code that runs when an element scrolls into view. The callback changed React state, and React drew the list again. They yielded before the state change. INP on that page fell by half, from 963 ms on phones.

Why it matters: none of these teams made the slow work itself faster. They ran it less often, or after the screen had changed. Lesson 28.1 teaches both moves.

Not every browser has scheduler.yield(). Trendyol fell back to setTimeout where it was missing:

On Ravi's page, fix 1 is the debounce from lesson 28.1:

Fix 1: search once the keys stopJavaScript
// debounce() is lesson 28.1's: it runs search once the keys stop for 250 ms.const searchSoon = debounce(search, 250);box.addEventListener("input", () => searchSoon(box.value.toLowerCase()));
  • Line 1 is a comment. debounce() is the helper from lesson 28.1.
  • Line 2 makes searchSoon, which runs search once the keys stop for 250 ms.
  • Line 3 calls it on every key, instead of the search itself.
The keys before and after fix 1Pop out in the code editor (opens in a new tab)JavaScript
// How long c, h, a and i took, in ms, and how many searches ran.const pages = [  { name: "As it was", keys: [347.4, 227.5, 419.5, 297.0], searches: 4 },  { name: "+ debounce", keys: [15.8, 11.5, 11.7, 12.7], searches: 1 },];for (const page of pages) console.log(`${page.name}: slowest key ${Math.max(...page.keys).toFixed(1)} ms, searches run: ${page.searches}`);
  • Lines 3 and 4 hold how long each of Asha's four keys took, and how many searches ran.
  • Line 6 prints As it was: slowest key 419.5 ms, searches run: 4 and + debounce: slowest key 15.8 ms, searches run: 1.

Every key now took under 20 ms. Why it matters: when a handler does heavy work on each key, run it once. That helps more than making it a little faster.

Someone else's code on every click

The second cause is harder to see, because it isn't your code. Cookie banners, trackers and chat widgets add listeners of their own. Many listen on the whole page, so every click runs them.

Erwin Hofman, a web performance consultant, timed the Accept button of OneTrust's cookie banner. One click ran several jobs. Closing the banner, the part the visitor sees, took 11.16 ms. Updating the tag manager took 190.35 ms. A tag manager is a script that loads and runs other tracking scripts. In his field data, 31% of clicks on that button were poor. His advice to banner makers: hide the banner first, then yield before the rest.

Disney+ Hotstar's TV app used a third-party library to move between cards with the remote. On every key press, it read the position of every focusable element. That made the browser lay out the page again and again. Lesson 6.3 called this layout thrashing. Hotstar built its own carousel, which reads sizes once per row of cards. Moving along a row went from about 400 ms to about 100 ms.

Some third parties slow clicks they never listen to. Poptin, a pop-up app on thousands of Shopify stores, made synchronous network requests as it started. Synchronous means the main thread stops until the answer comes. That made one task of about 900 ms on every page load, so any tap in it waited. Poptin made the requests asynchronous and started the app in a new task. INP across the stores went from 280 ms to 120 ms, at the 75th percentile.

Real-life analogyA survey at the till

You buy a tea, and the assistant is quick. But at the till, a market researcher asks you ten questions before you can leave. The shop did its part in seconds, and you still waited minutes. The fix isn't a faster assistant. It's a card you fill in at home.

In real life: The quick shop assistant
In JavaScript: The shop's own click handler
In real life: Ten questions before you can leave
In JavaScript: A third party's work inside every click
In real life: A card to fill in at home
In JavaScript: The third party's work after the next frame

On Ravi's page, fix 2 is a new version of the tracker that waits for the next frame:

Fix 2: the tracker waits for the frameJavaScript
const afterNextFrame = () => new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve, 0)));document.addEventListener("click", async function track(event) {  const report = { on: event.target.tagName, at: event.timeStamp };  await afterNextFrame();  work(30_000_000); // reads the page around the click  navigator.sendBeacon("/collect", JSON.stringify(report));});
  • Line 1 makes afterNextFrame(), from lesson 28.1. It waits for the next frame, then for a new task.
  • Line 3 notes what was clicked, as before.
  • Line 4 waits. Lines 5 and 6 then do the same work as before.
Where the tracker's work wentPop out in the code editor (opens in a new tab)JavaScript
// The tracker's work for the Add click, from Chrome's trace. Times in ms after the click.const now = { tracker: 130.3, nextFrame: 440.2 };const waits = { trackerStarted: 316.1, nextFrame: 318.0 };console.log(`Inside the click: the tracker ran ${now.tracker.toFixed(1)} ms; the frame came at ${now.nextFrame.toFixed(1)}`);console.log(`After the frame: the frame came at ${waits.nextFrame.toFixed(1)}; the tracker started at ${waits.trackerStarted.toFixed(1)}`);
  • Line 2 holds the Add click with fix 1 only. Line 3 holds it with fixes 1 and 2.
  • Line 4 prints Inside the click: the tracker ran 130.3 ms; the frame came at 440.2.
  • Line 5 prints After the frame: the frame came at 318.0; the tracker started at 316.1.

The tracker started once Chrome had finished the frame's work, and still did all its work: 137.4 ms. The click in the search box went from 143.6 ms to 15.5 ms. Why it matters: you can't always change a third party's code. But you can pick one that waits, ask its makers for a fix, or load it later, as lesson 24.2 showed.

Try it in your own browser. The tracker below keeps the main thread busy for 120 ms on every click:

Playground: a tracker inside the click, or after the frame
The playground's tracker, two waysJavaScript
const busy = (ms) => { const end = performance.now() + ms; while (performance.now() < end) {} };const afterNextFrame = () => new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve, 0)));lab.addEventListener("click", async function track() {  if (mode === "after") await afterNextFrame();  busy(120); // the stand-in tracker's work});
Inside the clickCart: 0
Mint tea
    Try it yourself
    When does the cart change?

    Click Add a few times. Then pick the other way and click again. Each click slower than 16 ms is listed with its time.

    Real clicks in your own browser, timed with Event Timing and rounded to 8 ms. The stand-in tracker keeps the main thread busy for 120 ms on every click. Numbers change from run to run, and with your computer's speed.

    Lines 4 and 5 of the panel's code are the whole difference. Inside the click, each click should take at least 120 ms. After the frame, the cart should count up at once.

    Drawing more than changed

    The third cause is drawing more of the page than changed. State is the data a page draws from, like the cart or the search words. When it changes, the code draws the page again. If it draws everything, the browser must style and lay out everything too.

    redBus, a bus ticket site in India, saw it in its search page's input fields. Each change called an expensive reducer. A reducer is a function that works out the new shared state. Then the page was drawn again. The team kept each field's text in the field itself. They told the shared store, Redux, only when the field lost focus. After that change, the page's INP was 72% better. Their INP work raised sales by 7%.

    Preply found two big parts of its search page drawn again whenever the filters closed. Neither ever changed, so wrapping them in memo was enough. memo skips a component whose inputs haven't changed, as lesson 28.3 showed. A banner's state sat at the top of the page, so closing the banner drew the whole page again. Moving that state into a small component fixed it.

    The Economic Times, a news site, had a page with 2,706 elements. A big page makes every style and layout slower. They built the menu's items only when the menu was clicked. The page lost about 1,200 nodes, the elements and text it's made of. With other changes, INP went from over 1,000 ms to 257 ms.

    Real-life analogyReprinting the whole menu to change one price

    A café raises the price of mint tea. One way is to reprint all forty pages of the menu. Another is a small sticker on one line. Both menus end up right, but only one takes all morning.

    In real life: One new price
    In JavaScript: One state change: a tea added to the cart
    In real life: Reprinting every page of the menu
    In JavaScript: Drawing all 3,000 rows again
    In real life: A sticker on one line
    In JavaScript: Changing only the badge and the button

    On Ravi's page, fix 3 changes only what changed:

    Fix 3: Add changes two thingsJavaScript
    teas.addEventListener("click", function addToCart(event) {  if (!event.target.matches(".add")) return;  cart.push(event.target.closest("li").dataset.tea);  document.querySelector("#cart").textContent = "Cart: " + cart.length;  event.target.textContent = "Added";});
    • Lines 1 to 3 are as before.
    • Line 4 changes the badge. Line 5 changes the clicked button's text. Nothing else is touched.
    The Add click before and after fix 3Pop out in the code editor (opens in a new tab)JavaScript
    // The Add click, with fixes 1 and 2 in place. Times in ms.const clicks = [  { name: "Draws everything", handlers: 75.1, after: 240.9, took: 318.0 },  { name: "Changes two things", handlers: 3.4, after: 66.6, took: 72.4 },];for (const c of clicks) console.log(`${c.name}: handlers ${c.handlers.toFixed(1)}, after them ${c.after.toFixed(1)}, took ${c.took.toFixed(1)}`);
    • Line 3 holds the Add click with fixes 1 and 2. Line 4 holds it with all three.
    • Line 6 prints Draws everything: handlers 75.1, after them 240.9, took 318.0 and Changes two things: handlers 3.4, after them 66.6, took 72.4.

    Drawing every row cost twice. First the handlers built 3,000 rows. Then the browser worked out the style of 12,000 elements and laid out 21,008 boxes before the frame. With fix 3, the browser still laid out the long list: 18.6 ms. Lesson 28.2 showed how to shrink that too. Why it matters: the cheapest work is work that never happens.

    What the fixes had in common

    Put the teams side by side, and the same steps come up.

    1. They found the slow interaction in the field first. Field data comes from real visitors' browsers. redBus, Hotstar and Preply used Google's web-vitals library, which measures INP in visitors' browsers. QuintoAndar split each slow interaction into its three parts.
    2. They made it happen again on a slow CPU. Trendyol slowed DevTools' CPU 4 times. Preply went from 6 times to 20 times.
    3. They fixed the slowest interaction first. The rest can wait.
    4. They measured again. Some changes did nothing. Preply removed 15% of its search page's code, and INP didn't move.
    5. They kept watching. QuintoAndar sends each release to a few users first, and rolls it back if it's slower.
    Real-life analogyA doctor asks where it hurts first

    You tell a doctor you feel unwell. A good doctor doesn't hand you every medicine on the shelf. She asks where it hurts, examines you, and treats that one thing. Next week, she checks again.

    In real life: Asking where it hurts
    In JavaScript: Field data: which interaction is slow for real visitors
    In real life: Examining you in the surgery
    In JavaScript: Making it happen again in DevTools, with a slowed CPU
    In real life: Treating that one thing
    In JavaScript: Fixing the slowest interaction first
    In real life: The check-up next week
    In JavaScript: Measuring again after the release

    Ravi's page shows why the order matters. Here is the visit's INP after each fix:

    INP after each fixPop out in the code editor (opens in a new tab)JavaScript
    // Each version's six interactions, in ms: Add, the search box, c, h, a, i.const versions = [  { name: "As it was", took: [452.8, 142.3, 347.4, 227.5, 419.5, 297.0] },  { name: "+ debounce", took: [440.2, 143.6, 15.8, 11.5, 11.7, 12.7] },  { name: "+ tracker waits", took: [318.0, 15.5, 14.8, 10.8, 10.9, 11.3] },  { name: "+ render less", took: [72.4, 17.3, 14.0, 12.7, 12.3, 11.0] },];for (const v of versions) console.log(`${v.name}: INP ${Math.max(...v.took).toFixed(1)} ms`);
    • Lines 3 to 6 hold each version's six interactions, in the order Asha did them.
    • Line 8 takes the slowest of each. It prints As it was: INP 452.8 ms, + debounce: INP 440.2 ms, + tracker waits: INP 318.0 ms and + render less: INP 72.4 ms.

    Fix 1 made four key presses fast, yet INP barely moved. The Add click was still the slowest, and INP reports the slowest. Fixes 2 and 3 worked on that click, and INP fell. Why it matters: start with the interaction your field data says is slowest.

    So which fixes paid off most? The ones aimed at the slowest interaction. The teams' businesses gained, too. redBus's sales rose 7%. Disney+ Hotstar's weekly card views went from 111 to 226 per user. Trendyol's clicks from its list to a product rose 1%. QuintoAndar's conversions rose 36% in a year, which it links strongly, but not only, to the better experience.

    The Add click and the slowest key on each version of Ravi's page, in three parts, to scale from 0 to 480 ms. As it was: Add took 452.8 ms (waited 2.1 ms, handlers 214.7 ms, after them 235.9 ms); the slowest key, a, took 419.5 ms (waited 105.3 ms, handlers 309.1 ms, after them 5.0 ms). + debounce: Add took 440.2 ms (waited 2.7 ms, handlers 206.8 ms, after them 230.7 ms); the slowest key, c, took 15.8 ms (waited 0.4 ms, handlers 6.5 ms, after them 8.9 ms). + tracker waits: Add took 318.0 ms (waited 2.0 ms, handlers 75.1 ms, after them 240.9 ms); the slowest key, c, took 14.8 ms (waited 0.4 ms, handlers 4.9 ms, after them 9.5 ms). + render less: Add took 72.4 ms (waited 2.3 ms, handlers 3.4 ms, after them 66.6 ms); the slowest key, c, took 14.0 ms (waited 0.6 ms, handlers 4.0 ms, after them 9.4 ms).As it wasAddhandlersafter452.8 mskey awaitedhandlers419.5 ms+ debounceAddhandlersafter440.2 mskey c15.8 ms+ tracker waitsAddafter318.0 mskey c14.8 ms+ render lessAdd72.4 mskey c14.0 ms0200 ms400 ms
    From the course's recording, to scale. Shaded: the handlers. Before them, the wait; after them, the browser's work until the next frame. Parts under 3 ms are too thin to draw. The dashed line is 200 ms.

    Pick a version to see all six of Asha's interactions and their three parts:

    Playground: Asha's six interactions, four versions
    Asha's six interactionsAs it was
    1. Addtook 452.8 mswaited 2.1 ms, handlers 214.7 ms, after 235.9 ms
    2. Search boxtook 142.3 mswaited 0.3 ms, handlers 134.0 ms, after 8.0 ms
    3. ctook 347.4 mswaited 0.5 ms, handlers 337.6 ms, after 9.3 ms
    4. htook 227.5 mswaited 63.7 ms, handlers 154.5 ms, after 9.3 ms
    5. atook 419.5 mswaited 105.3 ms, handlers 309.1 ms, after 5.0 ms
    6. itook 297.0 mswaited 138.1 ms, handlers 153.9 ms, after 5.0 ms
    Try it yourself
    As it was: INP 452.8 ms, set by the Add click

    Took is the interaction as INP counts it, to the next frame. Waited is the input delay, handlers the processing, and after the presentation delay. Each version keeps the fixes before it.

    From the course's recording in Chrome 157, with the CPU slowed 4 times: the middle of three visits for each version.

    Read this table across. Each row is one team: what was slow, what fixed it, and where this course teaches the fix.

    Eight teams, eight slow interactions
    TeamWhat was slowWhat fixed itThe lesson that teaches it
    PreplyTyping in the search page's subject box.Debounced the page's three input boxes.28.1, Fast event handlers.
    QuintoAndarClicking a filter on the property search.Showed a spinner first, yielded, then loaded the filter's data and opened its window.28.1, Fast event handlers.
    TrendyolInteractions on the product listing page.Yielded to the main thread before the setState call.27.2, Yielding to the main thread.
    OneTrust's cookie bannerClicking Accept on the cookie banner.Hide the banner first, then yield before telling the tag manager.28.1, Fast event handlers.
    Disney+ HotstarMoving between cards with the TV remote.Their own carousel, which reads sizes once per row of cards instead of once per card.28.2, Reducing presentation delay.
    Poptin, on Shopify storesClicks and taps while a store's page loads.Made the requests asynchronous, and started the app in a new task.27.2, Yielding to the main thread.
    redBusTyping in the search page's input fields.Kept each field's text in the field, and told the shared store only when the field lost focus.28.3, INP in frameworks.
    The Economic TimesThe whole site: INP close to 1,000 ms.Built the menu's items only when it's clicked, about 1,200 fewer elements, and loaded less important widgets later.28.2, Reducing presentation delay.

    Pick a team to see its own numbers, and where to read more:

    Playground: eight teams, eight slow interactions
    PreplyHeavy work on every key or filter
    What was slow
    Typing in the search page's subject box.
    Why
    Typing was painful on slow phones: most key presses took over 300 ms.
    The fix
    Debounced the page's three input boxes.
    What they measured
    INP while typing in the subject box went from 632 ms to 72 ms.
    Read it
    How Preply improved INP on a Next.js application (without React Server Components and App Router), Preply Engineering. Then lesson 28.1.
    Try it yourself
    Preply: heavy work on every key or filter

    The fix is taught in lesson 28.1, Fast event handlers.

    From each team's own write-up, in its own numbers. Each team measured its own visitors in its own way.

    Later, less often, or less?

    Each change below is one a team made. Sort it by what it does to the slow work.

    Later, less often, or less?
    • QuintoAndar shows a spinner, yields, then loads the filter's data
    • Trendyol yields before calling setState
    • Hide the cookie banner, then yield before telling the tag manager
    • Preply debounces its three input boxes
    • redBus tells its shared store only when a field loses focus
    • Hotstar's carousel reads sizes once per row of cards
    • The Economic Times builds its menu only when it's clicked
    • Preply wraps two heavy, unchanging components in memo
    Try it yourself
    0 of 8 correct

    Sort each team's change by what it does to the slow work.

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

    Common mix-ups

    • "Slow taps on a big site come from one big mistake." No. Preply fixed many small things, each worth 5 to 40 ms of INP.
    • "Shipping less JavaScript always lowers INP." No. Preply removed 15% of its search page's code, and INP didn't change. INP is about the work that runs during an interaction.
    • "A third party only costs time while it loads." No. OneTrust's banner spent 190.35 ms of one click updating the tag manager.
    • "Debouncing makes the search faster." No. On Ravi's page, the search took as long as before. It ran once instead of 4 times.
    • "Fixing most interactions fixes INP." No. Fix 1 made four key presses fast, but INP only went from 452.8 ms to 440.2 ms.
    • "If it's fast on my laptop, it's fast for visitors." No. Trendyol found its 700 to 900 ms task by slowing the CPU 4 times, to act like a phone. Hotstar's TV was slower than a laptop slowed 6 times.

    Practice exercises

    Answer first, then use the hints.

    Exercise 1 · Warm-upA pause while typing

    Asha stops for a moment after the h. How many times does the debounced search run?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // Asha types c, h, a, i. The search waits 250 ms after the last key.
    const pressedAt = [0, 120, 540, 660]; // she stops for a moment after h
    let searches = 0;
    pressedAt.forEach((time, i) => {
      const next = pressedAt[i + 1];
      if (next === undefined || next - time >= 250) searches += 1;
    });
    console.log("Searches: " + searches);

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

      Exercise 2 · Warm-upTell the store when the box loses focus

      This box tells its store only when it loses focus, as redBus's fields do. Asha types chai, then leaves the box. What does it print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      let reducerRuns = 0;
      const store = { dispatch: () => (reducerRuns += 1) };
      const box = new EventTarget();
      box.addEventListener("blur", () => store.dispatch({ search: "chai" }));
      for (const letter of "chai") box.dispatchEvent(new Event("input"));
      box.dispatchEvent(new Event("blur"));
      console.log("Reducer runs: " + reducerRuns);

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

        Exercise 3 · PracticeFix 3 first

        Ravi tries fix 3 alone on the page as it was. The search still runs on every key, and the tracker still works inside clicks. Which of Asha's interactions would most likely set INP now?

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

          Exercise 4 · PracticeThe h key's handlers

          In the six-interactions playground, pick + debounce. How long did the h key's handlers run, in ms?

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

            Exercise 5 · ChallengeTwo listeners in one click

            Open the course's trace of the page as it was in Chrome's DevTools. Two listeners ran inside the click on Add. Which one ran first?

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

              Exercise 6 · ChallengeA hook that didn't help

              Preply tried one React hook on an input box and saw no INP improvement. Which hook was it?

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

                Check your understanding

                For each question, ask which cause it is about, and which interaction sets INP.

                The INP fixes from real sites quiz · 7 questionsScore: first tries count
                1. Question 1 of 7On Ravi's tea list as it was, which of Asha's six interactions set the visit's INP?

                  Choose an answer to see the explanation.

                2. Question 2 of 7Debouncing the search made every key fast. Why did the visit's INP barely change?

                  Choose an answer to see the explanation.

                3. Question 3 of 7After the second fix, the tracker waits for the next frame. What happened to its work?

                  Choose an answer to see the explanation.

                4. Question 4 of 7With the first two fixes in place, the Add click was still slow. Where did most of its time go?

                  Choose an answer to see the explanation.

                5. Question 5 of 7In OneTrust's Accept click, which part took the longest?

                  Choose an answer to see the explanation.

                6. Question 6 of 7Preply removed a big part of its search page's code. What did that do to INP?

                  Choose an answer to see the explanation.

                7. Question 7 of 7What did most teams do before they changed any code?

                  Choose an answer to see the explanation.

                Key takeaways

                • Real slow interactions came from three causes again and again: heavy work on every key, someone else's code on every click, and drawing more than changed.
                • The fixes repeat too: show the change and do the rest later, run it less often, or do less of it.
                • Find the slowest interaction in the field first. INP reports the slowest, so fixing others may not move it.
                • A third party's work runs inside your clicks. Pick ones that wait, ask for fixes, or load them later.
                • Measure again after each change. Some changes, like shipping less code, may not help INP at all.

                Remember the one-liner.
                Find the slowest interaction, then show first, do it later, less often, or not at all.

                Sources

                The teams' numbers come from their own write-ups. The other facts come from these docs:

                Coming next: Memory and speed. Garbage collection pauses and memory pressure, and how they slow a page down.

                CompleteFrontend Clear concepts. Working examples.