Research Briefing · Frontend Performance · Aug 11, 2026

The Animation Tax: Why Animating Width Costs 273ms of Layout Per 1.8 Seconds

Two lines of CSS, same visual motion, wildly different bills. We traced Chrome to measure exactly what animating the wrong property costs — and it's worse than the layout tax.

Executive summary. We ran two versions of the same 3,000-element animation in Chrome 136 and traced the rendering pipeline. Animating width triggered 110 layout passes in 1.8 seconds — 273ms of layout work plus 213ms of paint. The identical animation using transform triggered zero layout passes. Same motion, one property swap, and the tax disappears. The rule that costs nothing to follow: animate transform and opacity, nothing else. Here's the measured breakdown, the traps (fill-mode, replay, will-change), and a 5-step audit.

Tax 1 — The Layout Invoice: Wrong Property = Reflow Every Frame

Animation is the most expensive place to trigger layout, because it triggers it repeatedly. Every frame of a width/top/height/margin animation invalidates geometry for the element and its dependents, and the browser must recompute before it can paint. We traced 1.8 seconds of a 3,000-element animation (each frame updating every element):

Animated propertyLayout passesLayout timePaint time
transform: translateX()1 (initial)~0.01 ms/frame49.6 ms
width110273.4 ms213.6 ms

Three ways to read the bill:

1. One reflow per frame, always. 110 frames of animation, 110 layout passes — width animation pays the layout tax on every single frame. The transform version pays it once, for the initial render, then the compositor takes over. That's not an optimization; it's the browser working as designed, and the design rewards transform.

2. 2.5ms of layout per frame before anything else. At 3,000 elements each frame costs ~2.5ms layout + ~2ms paint = ~4.5ms of rendering work against a 16.7ms budget (27%). Toss in any script work — a resize handler, a scroll listener, framework reconciliation — and the frame budget is gone. The transform animation spends ~0.45ms on paint (compositor-managed) and leaves the entire budget for your code.

3. The bill compounds over time. 273ms of layout in 1.8 seconds. Scale that to a 10-second animation and it's 1.5 seconds of pure blocking layout work — the browser can't respond to input during those flushes. Nobody writes a 10-second width animation on purpose; they write a 300ms one and ship it to 100,000 users a day.

Reproduce it: open the CSS Animation Generator, pick any preset, and inspect the keyframes — every one is transform/opacity only, by design. Then try adding a width keyframe and watch DevTools Performance fill the Layout track.

Tax 2 — The Paint Double-Bill: Layout Animations Re-Paint What They Reflow

Layout isn't the only line item. When width changes, the affected regions must be re-painted — 213.6ms of paint in our run, 4.3× the transform animation's paint cost. Transform animations are composited: the browser reuses the pre-painted layer and simply repositions it, so paint cost stays flat. Layout animations invalidate pixels and paint them again, frame after frame. The double-bill is why width animations feel heavy even on small screens with few elements — two render stages are both working, not one.

The practical translation: a width animation on a card grid re-paints every card's geometry each frame. The same motion with transform: scaleX() and a transform-origin touches only the compositor. If you need the element to actually change layout (other elements must reflow around it), that's a genuine layout animation — but it's a design decision, not an accident. Choose it with your eyes open.

Tax 3 — The Mobile Surcharge: Every Number Above × 4–6

A mid-range phone's CPU is roughly 4–6× slower than the desktop class machine we benchmarked. The desktop numbers above become the mobile reality: ~4.5ms/frame of rendering work → 18–27ms/frame — over budget on a 60Hz display before your JavaScript runs a single line. The width animation that "runs fine on my MacBook" drops frames on the phone in your user's pocket, every time it plays. The transform version scales to ~2-3ms/frame on mobile — still inside budget with room for your code. The mobile multiplier is the tiebreaker for every animation decision: if a fix is free (property swap), apply it; if it costs effort, the mobile threshold decides.

The Traps: Fill-Mode, Replay, and will-change

Fill-mode: the "blinking" entrance. Without animation-fill-mode: forwards, an element snaps back to its pre-animation style the instant the animation ends — a fade-in appears to flash and vanish. forwards holds the final keyframe's values; backwards applies the first keyframe during the delay (so a delayed fade-in starts invisible); both does both. Entrance animations: default to forwards.

Replay: same name, no restart. Re-applying an animation name doesn't restart it. The reliable pattern is remove, force reflow, re-add: el.style.animation = 'none'; void el.offsetHeight; el.style.animation = 'fadeIn 1s ease forwards';. This is the same forced-reflow mechanism from the layout tax — used here deliberately, once, per replay, not per frame.

will-change: a hint, not a drug. will-change: transform promotes the element to its own compositor layer ahead of time, which can smooth an animation's first frames. But every promoted layer costs GPU memory, and overusing it causes scrollbar jumps and layer-management overhead. Browsers auto-promote transform/opacity animations, so will-change is for one or two elements, removed when the animation ends — never a blanket rule on a component library.

Easing and duration: the UX side of the tax. ease-out front-loads motion and decelerates — the natural choice for entrances. linear is correct only for spinners. Entrances should finish in 200-400ms; over 500ms reads as sluggish. A quick ease-out arrival pays the same frame budget but feels instant.

Decision Framework: Is Your Animation Paying the Tax?

Animated propertyRender pipelineVerdict
transform (translate/scale/rotate)Compositor only✅ Always safe
opacityCompositor only✅ Always safe
width / heightLayout + paint every frame⚠️ Replace with scale()
top / left / marginLayout + paint every frame⚠️ Replace with translate()
color / backgroundPaint every frame🟡 Paint-only, OK sparingly
filter / box-shadowPaint + often GPU-heavy🟡 Use sparingly, avoid on many elements

5-Step Animation Audit

  1. Grep your CSS: search for @keyframes blocks containing width, height, top, left, margin, or padding. Every hit is a tax payment per frame.
  2. Replace with compositor equivalents: width → transform: scaleX() (+ transform-origin), top/left → transform: translate().
  3. Check fill-modes: entrance animations should use forwards (or both with delay) or they'll snap back.
  4. Audit will-change: remove any will-change on elements that aren't actively animating; keep it on one or two, max.
  5. Measure: DevTools Performance → run the animation → the Layout track should be empty. If you see Layout bars, you're still paying.

Frequently Asked Questions

Q: Is a transition on width as bad as an animation on width? Yes — the render pipeline doesn't care how you change the property. A transition on width pays the same per-frame layout and paint as a keyframe animation on width. The fix is the property, not the mechanism.

Q: My animation is fine on desktop — does any of this matter? It matters exactly when your users are on phones. The mobile multiplier (4-6×) turns a desktop-smooth width animation into a frame-dropping one. If you don't test on a mid-range Android, you're shipping blind.

Q: What about the Web Animations API (element.animate())? Same rules apply — element.animate() with transform/opacity is compositor-safe; with width it pays the same tax. The API is fine; the property choice is the whole game.

Q: Where do these numbers come from? Chrome 136 headless, desktop-class CPU, 3,000-element DOM, 1.8s of rAF-driven animation, rendered-pipeline tracing (Layout/Paint events via CDP Tracing), transform vs width versions of identical motion. Absolute values scale with hardware; the structural facts — one reflow per frame, zero for transform, the paint double-bill — hold everywhere.

The Verdict

The animation tax is the only frontend performance tax with a 100% free exemption: swap the property, keep the motion. transform: translate() instead of top, scale() instead of width, opacity for fades — every one of those swaps deletes a per-frame reflow and a re-paint. Our 273ms bill goes to zero without a single visible difference in the animation. When the tax is that easy to avoid, there's no excuse for paying it.

Related: The Layout Tax · The Image Weight Tax · The Bundle Tax · The Base64 Inflation · Tools: CSS Animation Generator · CSS Grid Generator · Flexbox Generator

References & Methodology

Benchmarks: jslet animation lab, Chrome 136 headless, Windows 11, 3,000 inline-block elements animated via rAF for 1.8s (transform: translateX vs width, identical oscillation), rendering-pipeline events collected via CDP Tracing (devtools.timeline category). Layout/Paint event durations summed; per-frame figures derived from event counts. Companion benchmark (reflow scaling, thrash) in The Layout Tax.

Compositor rule and will-change guidance per web.dev: Animations and performance and web.dev: opacity and transform animations.

All numbers are reproducible with the linked generator.

Cite as: jslet Research, 2026 — “The Animation Tax.” jslet.com. All benchmark data is original and reproducible with the linked tools.