Profiling CSS Animations in DevTools: Finding the Frame That Costs Too Much
"It feels janky" is not a bug report you can act on. Before you reach for will-change, add a translateZ(0) hack, or rewrite a keyframe set, you need to know which of the four rendering stages the browser is actually spending time in, and whether the animation is running on the compositor thread at all. This page, part of the Performance and GPU Acceleration section, is a click-by-click workflow for answering that in Chrome DevTools, with the Firefox and Safari equivalents at the end.
The narrow scenario: an animation that looks smooth on your machine and drops frames on a mid-range phone, and you need evidence about which stage is responsible before changing a line of CSS.
Why measure before you optimise
Every commonly repeated animation optimisation is conditional. will-change helps when an element genuinely needs its own compositor layer and hurts when it creates dozens of them, because each layer costs GPU memory and every extra layer costs compositing time. Promoting an element can increase paint cost by forcing text to re-rasterise. Swapping top for transform is nearly always right — but only after you have confirmed that layout is what is expensive, because if the frame is dominated by a heavy box-shadow repaint, the swap changes nothing.
Profiling also settles the one question static analysis cannot answer: whether an animation the browser could run on the compositor is actually doing so. Compositor eligibility is a runtime decision, and an animation can silently fall back to the main thread because an ancestor has a filter, because the element's layer was invalidated by a sibling, or simply because another property in the same rule forces layout. The tooling below distinguishes these cases in about a minute each, which is faster than reasoning about them.
The four stages, and where each property enters
A frame runs the same pipeline every time. Which stage your animated property enters at is what determines its per-frame cost.
A reproducible test case
Before profiling a real page, it helps to profile something whose answer you already know, so you learn what each panel looks like when it is telling you good news versus bad news. The two bars below animate to the same visual result by different means: the top one scales a transform, the bottom one animates width. Record both at once and the difference is unmistakable.
<p class="lane__title">transform: scaleX() — composite only</p>
<div class="lane"><div class="bar bar--transform"></div></div>
<p class="lane__title">width — recalculates layout every frame</p>
<div class="lane"><div class="bar bar--width"></div></div>
.lane__title { margin: 0 0 .35rem; font: 600 .85rem ui-monospace, monospace; }
.lane {
height: 34px;
margin-bottom: 1.25rem;
border: 1px solid currentColor;
border-radius: 6px;
overflow: hidden;
}
.bar { height: 100%; background: #7aa2ff; }
/* Cheap: transform is handled on the compositor thread, no layout, no paint. */
.bar--transform {
width: 100%;
transform-origin: left center;
animation: bar-scale 2s ease-in-out infinite alternate;
}
@keyframes bar-scale {
from { transform: scaleX(.15); }
to { transform: scaleX(1); }
}
/* Expensive: width is a layout property, so every frame runs Layout and Paint. */
.bar--width { animation: bar-width 2s ease-in-out infinite alternate; }
@keyframes bar-width {
from { width: 15%; }
to { width: 100%; }
}
The workflow, click by click
1. Record a trace of the animation only. Open DevTools, choose the Performance panel, set the CPU dropdown to 4x slowdown (6x if you are targeting low-end Android), then press the record button, trigger the animation, let it run two or three cycles, and stop. Keep the recording under about five seconds — longer traces are harder to navigate and add nothing.
2. Find the bad frames. The Frames track sits under the timeline. Each frame is a rectangle; green is a frame delivered on time, yellow means partially presented, and red means dropped. Drag-select across a run of non-green frames to zoom the rest of the panel onto exactly that window.
3. Read one frame's stack. With the range selected, look at the Main track. Inside a single frame you will see nested entries in pipeline order: Recalculate Style, Layout, Pre-Paint, Paint, Layerize, Commit. Hover any entry for its self time. A healthy compositor-driven animation shows a nearly empty main thread during the animation — the work is on the compositor and raster threads instead. The diagnostic signal is a Recalculate Style entry repeating once per frame, because that means the style engine is being asked to produce a new computed value on every tick, which is what a main-thread animation looks like.
4. Confirm what is repainting. Press Escape to open the drawer, choose the Rendering tab, and tick Paint flashing. Green rectangles now flash over every region being repainted. Watch the animation: the transform bar should produce no flashing once it has started, while the width bar flashes continuously. This is the fastest single check in the whole workflow, and it needs no recording.
5. Check layer promotion. In the same Rendering tab, tick Layer borders. Composited layers get an orange border, and tiles within them get a blue-grey grid. An element you expected to be promoted but that has no border is not on its own layer, which means its animation cannot be composited. For detail, open the Layers panel (via the ⋮ menu → More tools → Layers), click a layer, and read the sidebar: it reports the layer's memory estimate and, most usefully, the compositing reasons — the exact rule that caused promotion, or the absence of one.
6. Attribute the cost. Back in the Performance trace, select a Layout entry and read the summary pane's Nodes that need layout count and the first layout invalidation backtrace, which names the property that dirtied it. For paint cost, select a Paint entry and check its area — a paint covering far more than the animated element usually means the element is not isolated and is dragging its siblings into the repaint.
7. Fix, then re-record the same trace. Change one thing, record the same interaction with the same throttling, and compare frame colours. Optimisations applied without a second recording are guesses; this is also where you verify that a will-change hint helped rather than just added layers, which is covered in will-change and the compositor thread.
The key technique: spotting an animation that fell off the compositor
An animation of transform or opacity should be composited, but eligibility is checked at runtime and can be lost. The tell is always the same: per-frame Recalculate Style entries in the Main track during an animation that has no business touching the main thread. When you see that, work through the usual causes in order.
The most common is a second property in the same rule. transition: transform 300ms, height 300ms cannot be composited as a unit, because height requires layout — the whole animation is demoted. Split them or drop the layout property. Next is an ancestor that forces the element's content into its parent's layer, typically an animated filter or a clip-path higher up the tree. Then there is layer explosion: if a page has promoted hundreds of elements, the compositor may decline further promotion under memory pressure, and the Layers panel's memory estimate will show why. Finally, an animation driven by class changes in a script that also reads layout properties will interleave forced synchronous layouts, which show up in the trace as a distinctive purple Layout entry nested inside a scripting task — the classic layout thrash signature, and a strong argument for keeping the animation declarative, as discussed in when to use JavaScript animation.
Variation: the Animations panel and reduced-motion verification
Two more drawer tabs are worth knowing. The Animations panel (⋮ → More tools → Animations) captures animation groups as they fire and lets you replay them at 25% or 10% speed, drag the timing bars, and edit keyframe offsets live — invaluable for judging whether an easing curve feels wrong rather than whether it is slow. And in the Rendering tab, the Emulate CSS media feature prefers-reduced-motion dropdown forces the reduced state so you can confirm your guard actually collapses the motion; pair that check with the recipes in reducing motion preferences in CSS. A guard that is never tested is a guard that quietly stops working after the next refactor.
Browser support note
The workflow above describes current Chrome and Edge, where the Performance panel's Frames track and compositing-reasons sidebar are in their present form; older releases carry the same information under slightly different labels. Firefox offers the same analysis through its Performance profiler, whose markers include Styles, Reflow, and Rasterize, with paint flashing available as a toolbox setting and a Paint Flashing toggle in the Firefox developer tools settings pane. Safari's Web Inspector uses a Timelines tab: the Rendering Frames timeline gives the per-frame breakdown, the Layers sidebar in the Elements tab lists composited layers with their memory cost, and the Graphics section of Web Inspector's settings enables layer border display. The concepts transfer unchanged between all three — only the panel names differ.
FAQ
How do I tell whether an animation is running on the compositor? Record it in the Performance panel and inspect a frame from the middle of the animation. A compositor-driven animation leaves the main thread almost empty, while a main-thread one shows a Recalculate Style entry followed by Layout or Paint on every single frame.
What does paint flashing actually tell me?
It highlights in green every region the browser repaints. A well-behaved transform or opacity animation should produce no flashing once it has started, so a green rectangle pulsing over the animating element means you are repainting every frame.
Why does my animation profile fine on a desktop but stutter on a phone? Desktop machines have enough headroom to absorb layout and paint work that a mobile CPU cannot. Use the CPU throttling dropdown at 4x or 6x slowdown, which surfaces the long tasks that only appear on slower hardware.
Do Firefox and Safari have equivalents to these panels? Yes. Firefox's Performance profiler exposes the same style, reflow and rasterise markers plus a paint flashing toggle in its settings, and Safari's Web Inspector provides a Timelines tab with a Rendering Frames timeline and a Layers sidebar showing per-layer memory.
Related
- Performance and GPU Acceleration — the parent guide to compositing and animation cost.
- Optimizing CSS animations for 60fps — the fixes to apply once profiling has named the stage.
- will-change and the compositor thread — verify layer promotion hints with the Layers panel.
- When to use JavaScript animation — the tradeoff a layout-thrash trace usually points to.
- Aspect ratio for responsive media — remove the layout instability that makes traces hard to read in the first place.
Related articles
More pages in the same section.