Shots in this scene
Measuring Jank: DevTools and LoAF
In one line
Never fix jank by guessing. DevTools' Performance panel and Rendering tab show which frames were late and why, and the Long Tasks and Long Animation Frames APIs report slow work from your own code, with the script that caused it.
Think of it like this
A doctor does not treat "I feel unwell" with a guess. She takes a temperature, listens to the chest and asks where it hurts. Jank is the same: "it feels slow" is the symptom, and the tools below are the thermometer.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- long task
- A main-thread task longer than 50 ms.
- long animation frame (LoAF)
- A frame that took over 50 ms from the start of its work to being drawn, reported with the scripts that ran in it.
- blocking duration
- How much of a long frame was spent past the 50 ms mark, when the page could not respond well.
- INP
- Interaction to Next Paint: the time from a click, tap or key press until the next frame shows the result.
- User Timing
performance.mark()andperformance.measure(): names you give to stretches of your code so they show in DevTools.
Step by step
01Cause a long task and read the meter
In "Long tasks and a frame meter" press 120 ms task at 1×. The blue dot keeps moving (it is a transform animation), and the meter at the top shows a worst gap between frames of roughly 100 to 135 ms. It is close to the task's length, not exact, because frames start 16.7 ms apart.
02Cause a forced layout
Press Forced layouts (600 rows): the thrash loop of Topic 6.4. The meter jumps again. Each press adds 600 rows, so each is slower.
03Read LoAF's answer
The block "Long Animation Frames, from a real run" shows the code and what it printed in headless Chromium, in a top-level page, after the same two actions. The 120 ms task shows forced style and layout 0 ms. The loop shows 148 of its 153 ms forced. That difference tells a slow function from a thrashing one.
04Name your own work
performance.mark("a"), performance.mark("b") and performance.measure("update rows", "a", "b") name a stretch of your code. It shows on the Timings track of the Performance panel.
Try it yourself
- 1
Run the long task
In "Long tasks and a frame meter" press 120 ms task at 1×. The meter reads about 100 to 135 ms (in my six runs). Press Replay to reset it. The dot's track is green and it did not stop.
- 2
Compare a long task with a forced layout
In the LoAF output the click handler has
forced style and layout0 ms,layoutLoop148 of 153 ms. Only the second is fixed by reading first.
Code & diagrams
A 120 ms task starts after 200 ms and the Long Tasks API is checked after 900 ms. The meter changes from run to run, so it is not in the console output.
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
long tasks supported: true
long task seen: true
longer than 50 ms: trueNot live: LoAF exists only in Chromium browsers (123 and later), so the page does not depend on it. The first line is the page's own first frame. Your numbers will differ.
// In your own page (not in this preview frame, see the note): log every frame that took longer than 50 ms
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
console.log("long frame", Math.round(e.duration), "ms, blocking", Math.round(e.blockingDuration), "ms");
for (const s of e.scripts) {
console.log(" ", s.invokerType, s.invoker, s.sourceFunctionName || "(anonymous)",
"script", Math.round(s.duration), "ms, forced style and layout", Math.round(s.forcedStyleAndLayoutDuration), "ms");
}
}
}).observe({ type: "long-animation-frame" });
/* what it printed in headless Chromium, in a top-level page, after one 120 ms task and one thrashing loop of 600 rows:
long frame 61 ms, blocking 0 ms
long frame 122 ms, blocking 71 ms
event-listener BUTTON#s.onclick (anonymous) script 120 ms, forced style and layout 0 ms
long frame 157 ms, blocking 106 ms
event-listener BUTTON#f.onclick layoutLoop script 153 ms, forced style and layout 148 ms
*/Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Trust the animation, not the page
In "Long tasks and a frame meter" change the dot's keyframes from transform: translateX to left: 200px (add position: relative; left: 0; to .dot).
Myth vs fact
Myth
If DevTools shows 60 fps on my laptop, users see 60 fps.
Fact
Throttle the CPU and test a real phone. A 4× slower CPU makes a 15 ms layout take 60 ms or more (Topic 6.6), and any main-thread task longer than 16.7 ms drops a frame.
Remember this
- 1
The problem this solves: a team argues about whether an animation is slow, on their own fast computers. A measurement ends the argument, shows which culprit (JavaScript, style, layout, paint) is guilty, and later proves a fix worked. That is what Lena did with the hero.
- 2
The Performance panel. Record on a throttled CPU (Topic 6.6). The Frames lane marks frames presented on time, partly or dropped. On the Main track scripting is yellow and rendering purple: a long block over a frame is the cause. A Layout block nested inside your own function is a forced layout (Topic 6.4). The Animations track marks animations that cannot run on the compositor with a red triangle.
- 3
The Rendering tab. Open it from DevTools' three-dot menu, More tools, Rendering. Paint flashing flashes green over every repainted area, Layer borders outlines each layer in orange, and the Frame Rendering Stats overlay shows a live frame rate. A box that flashes green on every frame is repainting every frame.
- 4
The Long Tasks API. A long task is any main-thread task over 50 ms.
PerformanceObserverwith{ type: "longtask" }reports them from inside your page. It tells you when, not who. At 60 Hz a 120 ms task is about 7 missed frames. - 5
The Long Animation Frames API (LoAF). A long animation frame is a frame that took over 50 ms from the first task to the end of rendering. Each entry has
duration,blockingDurationand ascriptslist with the invoker, the function name, how long it ran andforcedStyleAndLayoutDuration. That is the who. It exists in Chromium browsers (Chrome 123 and later, Edge 123); check caniuse before relying on it elsewhere. Script details cover main-thread scripts only (not workers or cross-origin iframes). Run it in your own page. - 6
INP, the number users feel. Interaction to Next Paint measures how long a click, tap or key press takes to show its result. Google's thresholds are 200 ms or less for good, and more than 500 ms for poor. A 120 ms task started by a click can by itself eat most of that budget. The
web-vitalslibrary reports real values from your visitors.
Explain it without notes
How would you find out why one frame of an animation was late?
What does the Long Animation Frames API report that the Long Tasks API does not?
Practice
Write code that logs the slowest long-animation-frame script seen so far, with its function name.
Wrap the body of a function renderCards() in a User Timing measure, so it shows in the Performance panel.
Done when you can
You can find the late frame and its cause in the Performance panel
You can turn on Paint flashing and layer borders
You can tell a long task from a long animation frame, and know who LoAF names
You can name your own code with
performance.measure