Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 6.5Intermediate ~19 min · shot 5 of 7

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.

0/7 · 0%

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() and performance.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. 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. 2

    Compare a long task with a forced layout

    In the LoAF output the click handler has forced style and layout 0 ms, layoutLoop 148 of 153 ms. Only the second is fixed by reading first.

Code & diagrams

Long tasks and a frame meter

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.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

long tasks supported: true
long task seen: true
longer than 50 ms: true
Long Animation Frames, from a real runjs

Not 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).

terminal
$ Press 120 ms task and watch the dot
── what you'll see ──
The dot freezes for the whole task, and its track is red.
As in Topic 6.1: a `left` animation needs the main thread.

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

    The Long Tasks API. A long task is any main-thread task over 50 ms. PerformanceObserver with { 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. 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, blockingDuration and a scripts list with the invoker, the function name, how long it ran and forcedStyleAndLayoutDuration. 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. 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-vitals library reports real values from your visitors.

Explain it without notes

01

How would you find out why one frame of an animation was late?

02

What does the Long Animation Frames API report that the Long Tasks API does not?

Practice

01

Write code that logs the slowest long-animation-frame script seen so far, with its function name.

02

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