Shots in this scene
Monitoring Motion in Production
In one line
Lab tests show that motion works on your machine; field data shows it works for your visitors. Measure Interaction to Next Paint and Long Animation Frames from real users with web-vitals, set a motion budget, put heavy effects behind a flag, and make a regression after a library update show up as a failed number.
Think of it like this
A restaurant tests recipes in its own kitchen but learns the truth from the diners' tables: cold plates, long waits. The kitchen is the lab, the tables are the field, and only one tells you what customers feel.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- field data
- Measurements from real visitors' browsers. Lab data comes from your own machine or a test.
- INP
- Interaction to Next Paint: the time from a tap, click or key press to the next painted frame, roughly the slowest of the visit.
- LoAF
- Long Animation Frame: an entry for any frame that took more than 50 ms, with the scripts that ran in it.
- attribution
- Extra data that says why a number is what it is: which element, which script, which part of the delay.
Step by step
01Report INP with its cause
Import the attribution build and send each result with sendBeacon. Keep the object small: the number, the target, the three parts and the longest script.
`/analytics` is a placeholder for your own endpoint. `web-vitals` 6.2.3.
import { onINP } from "web-vitals/attribution";
onINP(({ name, value, rating, attribution: a }) => {
navigator.sendBeacon("/analytics", JSON.stringify({
name, value, rating,
target: a.interactionTarget,
inputDelay: a.inputDelay, processing: a.processingDuration, presentation: a.presentationDelay,
script: a.longestScript?.entry.sourceFunctionName, // empty where Long Animation Frames are not supported
}));
});02See a real report
We made a page with a 180 ms click handler (addToCart) and one forced layout loop, loaded the attribution build in Chromium and clicked once. INP was 184 ms and rated good: a handler that feels slow can sit inside the threshold. The cause is in the same object.
03Write the budget
Put the limits in one object and check every release: bytes in CI, INP and long frames on a dashboard.
04Put the heavy effect behind a flag
Combine the flag, reduced motion, Data Saver and device hints in one function, and write down what an unknown hint means.
Try it yourself
- 1
Change a phone
In "A budget and a flag" set
saveData: truefor the launch laptop:off (data saver). Setcores: 2instead:off (under 4 cores). The first failing rule wins. - 2
Make the update pass
Change the "after" numbers to
{ inp: 190, longFrames: 4, scriptKB: 35 }. The line sayspass: a value exactly on the limit (35) passes, because the check uses>.
Code & diagrams
A 120 ms busy loop, like a heavy click handler. The `longtask` observer sees it. `long-animation-frame` is listed as supported but no entries arrive here, so the real attribution output is in the walkthrough.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
longtask listed: true, long-animation-frame listed: true
long tasks seen: 1, the longest over 50 ms: true, long animation frames seen: 0One heavy effect decided for five visitors (a missing memory hint is unknown, not low), then two releases checked against a budget.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
launch laptop on
Lena's cheap phone off (under 4 GB memory)
Ravi off (reduced motion)
kill switch pulled off (flag)
browser hides memory on
before the library update pass
after the library update FAIL: inp 260 > 200, longFrames 9 > 5, scriptKB 45 > 35Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Treat a missing memory hint as zero
In "A budget and a flag" change if (memory !== undefined && memory < 4) to if ((memory ?? 0) < 4).
Myth vs fact
Myth
If INP is good in the lab, it is good for users.
Fact
Your lab has a fast machine and few tabs. Field data comes from many devices; the slow tail is where a motion problem lives.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Send one beacon per metric per page view, and sample at high volume.
- ▸
longAnimationFrameEntriesis empty on browsers without the API, and frames under 50 ms never get an entry. In theweb-vitalstypings,totalUnattributedDurationholds scripts under 5 ms and time in frames under 50 ms, and it isundefined(not everything) when no long frame was seen. A large value means "slow, but not in a long frame we can name".
Remember this
- 1
The problem this solves: your visitors use thousands of phones, busy tabs and slow networks that you never test. A library update that slows every tap shows up on nobody's laptop, and without measurement you hear about it from a complaint a week late.
- 2
Interaction to Next Paint (INP) measures the time from a visitor's tap, click or key press until the browser paints the next frame, roughly the slowest of the visit. It has three parts: input delay (the main thread was busy), processing duration (your event handlers) and presentation delay (style, layout, paint, compositor).
web-vitalsrates it good at 200 ms or less and poor above 500 (INPThresholdsin its source). Judge a percentile of many visits, not one: Google's guidance is the 75th. - 3
Long Animation Frames (LoAF). A frame over 50 ms gets an entry with the time spent in scripts, style and layout and, per script, who started it (
invoker, such asBUTTON#buy.onclick) and the function name. That turns "INP was 260" into "addToCartran for 180 ms". Where it is unsupported,web-vitalsgives an emptylongAnimationFrameEntries(check caniuse). The preview lists the type but delivers no entries (the first example prints 0), so the real output below comes from a normal page. - 4
The attribution build.
web-vitals/attributiongives the number plus the cause:interactionTarget, the three parts,loadState, the LoAF entries,longestScriptand script, style-and-layout and paint totals. It costs a little:onINPis 2,048 bytes gzipped in the plain build and 3,522 in the attribution one. Send results withnavigator.sendBeacon, which survives the page closing. - 5
Budgets. A motion budget is a set of limits a release must stay under: INP, long frames per session, script bytes for the tools of Topic 18.3. It turns "feels slower" into "INP went from 180 to 260, over 200". The second example prints a pass for the old release and a named failure for the new one. Put the byte part in CI so a pull request that adds 14 KB fails before it merges.
- 6
Flags for heavy effects. A heavy effect (a particle burst, a big blur) should run only when a flag is on and the visitor can afford it: not reduced motion, not Data Saver, enough memory and cores.
navigator.deviceMemoryandnavigator.connection.saveDataare Chromium-only and often missing, so decide what unknown means (the example runs the effect: missing is not low) and keep a kill switch, which turns an effect off for everyone without a deploy. - 7
A regression after a library update: the CI bundle check is a few KB over, a field chart shows INP creeping up on Android, attribution names one invoker, and you flip the flag off while you investigate. Without them you only have the complaint.
Explain it without notes
What is the difference between INP and a Long Animation Frame entry?
Which three pieces would have caught a library update that slowed every tap?
Practice
Make check() report no data instead of failing when a metric is undefined.
Why does the budget check say no data instead of counting a missing metric as 0?
Done when you can
You can say what INP and a Long Animation Frame are, and what each is good for
You can read an attribution report and name the slow script
You can write a motion budget and a check that names what failed
You can decide a heavy effect with a flag, reduced motion and device hints, and handle unknown hints