Profiling Scroll Animations in Chrome DevTools
A scroll-driven animation that looks smooth on your development machine can still be one long task away from jank on a mid-range phone, and the only way to know is to record a trace and read it. This guide covers the full Chrome DevTools workflow for scroll-driven animations — the Performance panel recording procedure, frame timing analysis, compositor-thread verification, the Animations drawer, and the Layers panel and 3D view — plus the equivalent tooling in Firefox and Safari. It is the measurement companion to the rest of the Animation Performance Profiling & Optimization section: profile first, then apply the compositor-safe property and will-change budgeting rules to whatever the trace exposes.
Pages in this guide
Performance panel reference
The Performance panel presents a recorded trace as parallel tracks. For scroll-driven animation work, five of them matter:
| Track | What it shows | What to look for |
|---|---|---|
| Frames | One box per presented frame, with duration | Dropped and partially presented frames during the scroll gesture |
| Interactions | Input events and their processing spans | Scroll begin/update events overlapping long tasks |
| Main | Flame chart of main-thread tasks: JS, style recalc, layout, paint | Red-flagged long tasks and Forced reflow warnings while scrolling |
| Thread pool / Compositor | Compositor-thread activity | Continuous per-frame animation ticks with no gaps |
| GPU | Raster and draw work on the GPU process | Sustained spikes that suggest oversized layers or repeated re-raster |
The single most important reading skill is vertical alignment: a dropped frame in the Frames track almost always sits directly above a long task in the Main track. The rendering pipeline for scroll animations explains why — style, layout, and paint are main-thread stages, so anything that forces them per scroll event competes with frame production, while a compositor-driven animation-timeline advances without touching the main thread at all.
Minimal working example
Profiling is easier to learn on a page where you know exactly what should happen. This test page animates a progress bar with scroll() and reveals cards with view() — both compositor-eligible, both driven purely by CSS:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
/* Progress bar bound to document scroll */
.progress {
position: fixed;
top: 0; left: 0;
width: 100%; height: 4px;
background: currentColor;
transform-origin: 0 50%;
animation: grow linear both;
animation-timeline: scroll(root block);
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* Card reveal bound to its own viewport visibility */
.card {
opacity: 0;
transform: translateY(2rem);
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 60%;
}
@keyframes reveal {
to { opacity: 1; transform: none; }
}
</style>
</head>
<body>
<div class="progress"></div>
<main style="height: 400vh; padding-top: 50vh;">
<div class="card">Card one</div>
<div class="card" style="margin-top: 80vh;">Card two</div>
</main>
</body>
</html>
The recording workflow, step by step:
- Open the page in an incognito window so extensions cannot inject content scripts into the trace.
- Open DevTools → Performance. In the panel’s gear menu, set CPU: 4x slowdown to approximate mid-range mobile hardware.
- Click Record (not “Record and reload” — you want steady-state scroll behavior, not load behavior).
- Scroll through the full animated range at a realistic speed, including at least one fast fling.
- Stop after three to five seconds. Longer traces bury the signal; short, focused recordings are easier to read.
On this test page the expected trace shape is: a quiet Main track, a continuously active compositor track, and a full row of green frames. Any deviation from that shape on your real pages is the thing to investigate.
Frame timing: reading the Frames track
A display refreshing at 60 Hz gives you a 16.7 ms budget per frame; a 120 Hz display halves that to 8.3 ms. The Frames track renders each presented frame as a box whose width is its duration:
- Green frames were produced within budget.
- Yellow (partially presented) frames mean the compositor shipped a frame but the main thread had not delivered updated content for it — during scroll this often looks acceptable because compositor-driven animations still advanced, but any main-thread-dependent updates visibly lag.
- Red or dashed (dropped) frames mean nothing new reached the display for that vsync interval.
Two frame-timing patterns are diagnostic for scroll-driven work:
Isolated drops aligned with long tasks. One dropped frame under one 60 ms task points to a discrete cause — a scroll handler, a layout thrash, a large style recalculation. Click the task and read its flame stack bottom-up.
Rhythmic drops every few frames. A repeating drop pattern usually means per-frame work slightly over budget — for example painting a large layer each frame because the animated property is not compositor-eligible. Check whether the trace shows Paint and Rasterize entries recurring during scroll; if so, the fix lives in which properties you animate, not how you profile.
If the visual result looks wrong rather than slow — easing feels off, ranges fire early or late — that is a timeline configuration problem, not a performance problem; the guide to debugging scroll animation timing functions covers that separately.
Verifying compositor execution
The core question for any scroll-driven animation is binary: does it tick on the compositor thread, or does it fall back to the main thread? The trace answers it three ways.
1. Main track silence. During a scroll gesture over a compositor-driven animation, the Main track should show essentially nothing attributable to the animation — no Recalculate Style, no Layout, no Paint entries recurring per frame. Recurring per-frame main-thread entries mean the animation (or something else bound to scroll) is running on the main thread.
2. Compositor track activity. The compositor thread track shows a steady tick per frame while the animation is in range. A compositor-driven animation-timeline: scroll() animation keeps ticking even while the main thread is blocked — you can prove this by running a synthetic 500 ms busy-loop in the console while scrolling: the progress bar keeps moving, and the trace shows compositor activity spanning the main-thread stall.
3. The green-overlay spot check. DevTools → Rendering drawer → Scrolling performance issues highlights regions that force main-thread involvement in scrolling (non-passive listeners, slow-scroll regions). Separately, checking Layer borders draws orange/olive borders around composited layers, and Paint flashing flashes green on every repaint. The success criterion during scroll: layer borders visible around animated elements, no green paint flashes on them. Green flashing on every scroll frame means paint is happening per frame — the animation is not running purely on the compositor.
Compositor fallback has specific causes worth memorizing: animating a layout- or paint-affecting property (width, top, box-shadow spread), animating a compositor-eligible property alongside a non-eligible one in the same keyframes, or an effect the engine cannot composite (some filter values, clip-path shapes). The trace shows the symptom (per-frame main-thread work); the property choice is the cause.
The Animations drawer
The Animations drawer (⋮ → More tools → Animations, or Ctrl/Cmd+Shift+P → “Show Animations”) captures animations as they start and lists them as scrubable groups. For scroll-driven animations Chrome extends the drawer in two useful ways:
- Captured scroll-driven animations display with their timeline source and range rather than a wall-clock duration, and scrubbing the group scrubs scroll position, letting you step an
animation-range: entry 0% entry 60%reveal pixel by pixel without touching the page. - Selecting an animation highlights the target element in the viewport and reveals its keyframes, so you can confirm the browser resolved the intended
view()subject — a frequent surprise when a wrapper element, not the visual card, ended up as the timeline subject.
The drawer complements the Performance panel rather than replacing it: the drawer answers “is this animation attached to the timeline I intended, over the range I intended,” while the trace answers “what does it cost.”
Layers panel and the 3D view
Every composited animation implies at least one promoted layer, and layers cost GPU memory — roughly width × height × 4 bytes each at device resolution. The Layers panel (and its successor, the 3D View drawer, which merges layers, z-index, and DOM depth into one rotatable scene) shows you exactly what got promoted:
- Open ⋮ → More tools → Layers (or 3D View in recent Chrome versions).
- Rotate the scene and locate your animated elements. Each should sit on its own thin slab.
- Select a layer to read its memory estimate and, critically, its compositing reason —
will-change: transform,active animation,overlaps composited layer, and so on.
Two failure smells appear here first. Layer explosion: dozens of unintended layers created because promoted elements overlap siblings, forcing the engine to promote those siblings too — common when a long list of cards each carries will-change. Oversized layers: a full-page wrapper promoted because the animation was declared on a container rather than the visual element, allocating megabytes for a 40-pixel effect. Both are budgeting problems addressed in the compositor-safe properties and will-change guide; the Layers view is how you catch them.
Firefox and Safari equivalents
Cross-browser profiling matters even while scroll-driven animation support is uneven, because your fallback paths (IntersectionObserver, scroll listeners) have main-thread costs of their own.
Firefox. The DevTools Performance panel is a front end to the Firefox Profiler. Record with the “Web Developer” preset, then read the per-thread timeline: the Renderer/Compositor threads correspond to Chrome’s compositor track, and the marker chart shows Styles, Reflow, and Rasterize markers on the content thread. Note that animation-timeline in Firefox is behind a pref (Nightly), so on release Firefox you are profiling your fallback, not the scroll-driven animation itself — which is exactly why the recording is worth doing. The stack-sample view (call tree, inverted) is Firefox’s answer to Chrome’s bottom-up tab and is often better at attributing scroll-handler cost to specific functions.
Safari. Web Inspector’s Timelines tab is the equivalent recorder. Enable the Frames timeline mode to get per-frame rows broken into script, style, layout, and paint segments — a frame exceeding budget shows which segment blew it. The Layout & Rendering timeline flags forced synchronous layouts, and the Graphics tab lists running animations. Elements → Layers sidebar badge shows per-element compositing reasons and layer memory, mirroring Chrome’s Layers panel. Safari’s support for scroll-driven animations is in active development (visible in Technology Preview builds), so as with Firefox, profile both the native path where available and your fallback path on current release Safari.
For a broader view of which engines run what, see the browser support and progressive enhancement guide.
Gotchas and failure modes
-
Profiling with DevTools docked into the same window. Rendering DevTools itself costs frames in the profiled renderer on some setups. Undock DevTools into a separate window before recording scroll traces.
-
Trusting a trace recorded without CPU throttling. A desktop CPU absorbs a 12 ms scroll handler invisibly; a mid-range phone does not. Record at 4x (or 6x) slowdown as your default, and treat unthrottled traces as best-case only.
-
Scrolling with the mouse wheel only. Wheel, trackpad, touch drag, and keyboard scrolling take different input paths and different event frequencies. A page that traces cleanly under wheel scrolling can jank under touch-drag because touch generates far more frequent updates. Record at least wheel and touch (device emulation with touch enabled).
-
Confusing “compositor is busy” with “compositor is animating.” The compositor track also shows plain scroll handling for any page. The proof of compositor-driven animation is the combination: compositor ticks per frame plus a Main track with no per-frame animation work plus visual progress during a deliberate main-thread stall.
-
The Animations drawer misses animations that started before it opened. The drawer only captures groups created while it is listening. Reload or re-trigger the animation after opening the drawer, or the list stays empty and you will wrongly conclude the timeline never attached.
-
will-changeleft in place during profiling experiments. If you sprinklewill-changewhile testing, layers persist and later traces lie to you — memory stays elevated and paint behavior changes. Reset the experiment before each recording; layer promotion should come from your committed CSS, not console leftovers. -
Reading
requestAnimationFramecost as animation cost. Third-party scripts (analytics, smooth-scroll libraries) often run per-frame rAF loops. In the bottom-up view, group by activity and check the source URL before attributing per-frame cost to your animation code.
Performance checklist
- Record every scroll trace with 4x CPU throttling, DevTools undocked, in an incognito window.
- Keep recordings between three and five seconds, containing at least one fast fling and one slow drag.
- Frames track: zero dropped frames during scroll on the throttled profile is the target; investigate any drop that vertically aligns with a Main-track task.
- Main track: no recurring per-frame
Recalculate Style,Layout, orPaintentries attributable to the animation. - Rendering drawer: layer borders on around animated elements, paint flashing silent during scroll.
- Layers / 3D view: every promoted layer has an intentional compositing reason and a memory estimate you can justify; no layer explosion across list items.
- Animations drawer: each scroll-driven animation attaches to the intended timeline and range, and scrubs cleanly.
- Re-run the same scenario in the Firefox Profiler and Safari Timelines, profiling the fallback path where
animation-timelineis not yet available. - Feed the results forward: main-thread findings go to the main-thread jank diagnosis workflow, and any user-facing regression should be checked against the Core Web Vitals impact of animated interfaces.
Related
- The Rendering Pipeline for Scroll Animations — the style → layout → paint → composite model that every trace in this guide is a picture of
- Animating transform vs top/left in Scroll-Driven Effects — the property-choice fix for per-frame layout work found in traces
- How View Transitions Affect INP — Measurement and Mitigation — extending this profiling workflow from scroll gestures to interaction latency
- Understanding the CSS Scroll Timeline API —
scroll(),view(), andanimation-rangesyntax for the animations you are profiling - When to Use CSS Animations over JavaScript Libraries — what the compositor-vs-main-thread distinction means for architecture choices