Animation & Compositing Performance: Smooth Motion Without Main-Thread Work

This topic is part of Rendering & CSS Performance. Smooth animation requires producing a new frame every 16.7ms on a 60Hz display, or every 8.3ms at 120Hz. Whether that is easy or impossible depends on what each frame has to do. If an animation changes a property that affects layout, each frame needs style recalculation, layout, paint and compositing, all coordinated by the main thread, which is also running JavaScript, handling input and parsing data. If the main thread is busy for 50ms, the animation stutters.

Browsers have a way out: the compositor. Pages are drawn into layers, and the compositor thread (with the GPU) assembles those layers into the final frame. Changes to a layer's transform or opacity — and a few other properties such as some filters — can be applied by the compositor alone, without asking the main thread to re-run style, layout or paint. Animations of those properties keep running smoothly even when JavaScript is busy. This is why the core rule of animation performance is: animate transform and opacity, and avoid animating layout properties like width, height, top, left and margin.

Layers themselves have costs — memory and compositing time — so promoting everything is counterproductive. And modern CSS adds new tools: scroll-driven animations that run on the compositor without scroll listeners, and the View Transitions API, which animates between DOM states using snapshots. Each has its own performance profile, covered in the guides of this topic.

Cost of animating different CSS properties Which rendering stages run each frame when animating different kinds of CSS properties. Cost of animating different CSS properties Property type Layout Paint Composite only width, height, top, left, margin every frame every frame no color, background, box-shadow skipped every frame no transform, opacity skipped skipped yes, on its own layer filter (many), clip-path (some) skipped depends browser-dependent

The Metric Degradation This Topic Addresses

Animation problems show up first as jank: visibly uneven motion, dropped frames, stuttering scrolling. Chrome reports dropped frames in the Performance panel's Frames track, and some RUM tools collect smoothness metrics. They also show up in Core Web Vitals. Animations of layout properties cause layout shifts that count towards CLS when not triggered by user input within the last 500ms — a banner sliding in by animating top, or an expanding element pushing content down. Main-thread animations compete with input handling, adding to INP: an interaction during a JavaScript-driven animation has to wait for animation frames to finish. Heavy layer usage on low-memory devices can cause the browser to drop layers or even crash tabs.

Prerequisites

  • A Performance panel recording of the animation or interaction, with CPU throttling, and the Frames track visible.
  • The Rendering drawer in DevTools: paint flashing, layer borders and the frame rendering stats overlay.
  • The Layers panel, to inspect layer count and memory.

1. Environment Setup: Make Rendering Work Visible

Enable Paint flashing to see what repaints during an animation (green flashes on areas being painted), Layer borders to see compositing layers, and Frame Rendering Stats for a live FPS meter. Throttle CPU to 4x or 6x to approximate a mid-range phone.

2. Capture a Baseline

Record the animation in the Performance panel. In the Frames track, note dropped and partially presented frames. In the Main track, note whether each frame contains Recalculate Style, Layout and Paint events — their presence means the animation is not compositor-only.

3. Isolate the Animated Properties

List the properties each animation changes, from CSS (transition, @keyframes) or JavaScript (style changes in requestAnimationFrame, Web Animations API, animation libraries). Identify those that trigger layout or paint.

javascript
// List running animations and the properties they animate.
document.getAnimations().map((a) => ({
  target: a.effect?.target?.className,
  props: a.effect?.getKeyframes().flatMap((k) => Object.keys(k)).filter((p) => !['offset', 'easing', 'composite', 'computedOffset'].includes(p)),
}));
// trade-off: this only lists CSS and Web Animations; JavaScript loops that set
// styles directly do not appear and must be found in the Performance panel.

4. Apply: Move Motion to transform and opacity

Rewrite animations to use transform (translate, scale, rotate) and opacity. Use will-change sparingly for elements about to animate. Replace scroll-linked JavaScript with scroll-driven animations. Respect prefers-reduced-motion.

css
/* Slide-in panel: compositor-only. */
.panel { transform: translateX(100%); transition: transform 280ms cubic-bezier(.2,.8,.2,1); }
.panel.open { transform: translateX(0); }
/* Fade + scale for a dialog. */
@keyframes pop { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
.dialog[open] { animation: pop 180ms ease-out; }
@media (prefers-reduced-motion: reduce) { .panel, .dialog[open] { transition: none; animation: none; } }
/* trade-off: transform does not change layout, so siblings do not reflow around
   the moving element; designs that need content to move aside need another approach. */

One frame: layout animation vs compositor animation Timeline of work in a single frame for an animation of left versus an animation of transform. One frame: layout animation vs compositor animation Animating left style layout paint comp. Animating transform composite 0ms 4ms 8ms 12ms 16ms 20ms 60Hz budget

Deconstructing the Compositor

When the browser paints, it records drawing commands for layers. Some elements get their own layer (for example those with 3D transforms, video, canvas, will-change: transform, or elements animating transform or opacity). The compositor rasterises layers into tiles (bitmaps), often on the GPU, and composites them into frames. For a compositor-only animation, each frame simply reuses existing tiles and applies a different transform or opacity to the layer — no main-thread work, no repainting.

This only works if the layer's content does not change. If the animation also changes, say, background-color, the layer must be repainted on the main thread each frame. If it changes width, layout must run first. And if the element is not already on its own layer when the animation starts, the browser may need to create a layer and paint it, which can cause a hitch on the first frame — this is what will-change is for.

Layers cost GPU memory roughly proportional to their pixel area (width × height × 4 bytes, times device pixel ratio squared). A full-screen layer on a 3x phone can take tens of megabytes. Hundreds of layers, or a few very large ones, can exhaust GPU memory on low-end devices, leading to slow compositing or checkerboarding.

Advanced Diagnostics and Edge Cases

Implicit layer promotion. Elements that overlap a composited layer may be promoted too ("layer squashing" and overlap reasons). The Layers panel shows the reason for each layer.

Animating box-shadow. Shadows are expensive to paint. Animate the opacity of a pseudo-element that carries the final shadow instead.

Animating height for accordions. A layout animation by nature. Alternatives: animate grid-template-rows from 0fr to 1fr (still layout, but in a contained area), use interpolate-size where supported, or use transforms for a reveal effect.

JavaScript animation libraries. Many animate via inline styles on the main thread. Prefer libraries or modes that use the Web Animations API with transform and opacity, which can run on the compositor.

Main-thread-dependent compositor animations. Some animations (for example, those with steps() timing or certain filters) may fall back to the main thread. Check the Animations panel for "not composited" hints.

Validation and Budgeting

Validate with the Frames track (no dropped frames during the animation under throttling), Paint flashing (no green flashes on the animating element after the first frame), and the Layers panel (reasonable layer count and memory). In the field, monitor CLS attributed to animated elements and INP during animations.

CheckTarget
Dropped frames during key animations (4x throttle)None visible
Paint during animation framesNone after first frame
Layer count on key pagesTens, not hundreds
CLS from animated elements0

Worked Example: A Mobile Navigation Drawer

An e-commerce site's mobile navigation drawer animated left from -300px to 0 and dimmed the page with an overlay animating background-color. On a mid-range Android phone, the drawer dropped about 40% of frames, and when product recommendations were rendering in the background, the animation froze for 200ms. The team switched the drawer to transform: translateX() and the overlay to opacity on a pre-rendered black layer, adding will-change: transform only while the drawer was open or opening. Dropped frames fell to near zero even while the main thread was busy, because the compositor ran the animation independently. INP for the menu button improved from 260ms to 120ms, because the click handler no longer started a main-thread animation loop.

Dropped frames opening a navigation drawer (mid-range phone) Bar chart of dropped frames for a navigation drawer animation before and after moving to transform and opacity. Dropped frames opening a navigation drawer (mid-range phone) left + background-color 40% left + opacity overlay 26% translateX + opacity overlay 1%

Choosing an Animation Technology

CSS transitions and keyframes are the default for UI state changes: declarative, compositor-friendly for transform and opacity, and automatically paused when offscreen or in background tabs. The Web Animations API gives JavaScript control (play, pause, reverse, sequence) with the same performance characteristics as CSS animations. requestAnimationFrame loops are needed for physics, gestures and canvas drawing; keep them doing as little as possible, writing only transforms. Scroll-driven animations replace scroll listeners for scroll-linked effects. View Transitions handle animated changes between page states, including cross-document navigations. Libraries that wrap these (for example those built on WAAPI) inherit their performance; libraries that interpolate values in JavaScript and write styles each frame are main-thread bound.

Animations and Accessibility

Performance and accessibility align on motion. Users who set prefers-reduced-motion should get reduced or no motion; that also removes animation work on devices where users often choose reduced motion for comfort or battery. Avoid animations that cause large layout shifts, flash, or move content the user is reading. Ensure animations do not delay access to content: a page that fades in its main content over 600ms delays LCP perception and can delay LCP itself if opacity starts at zero.

Animations and LCP

Entrance animations on the hero are a common, avoidable LCP regression. An element animated from opacity: 0 is not considered painted for LCP until it becomes visible, so a 1-second fade-in on the hero headline or image can add up to a second to LCP. Hero content should be visible immediately; if motion is required, animate a transform from a nearly final position with opacity starting high, or animate secondary decorative elements instead. Slide-in animations that start offscreen have the same problem: the element is not in the viewport at first paint.

Measuring Smoothness in the Lab and Field

Frame rate alone is a crude measure; what users notice is irregularity. In the lab, the Performance panel's Frames track marks each frame as presented, partially presented or dropped, and the summary shows how many frames missed their deadline during the recording. Record the same animation several times under the same throttling and compare the share of dropped frames. Pay attention to the first frames of an animation, where layer creation and painting cause hitches even for compositor-friendly animations.

In the field, there is no standard smoothness metric in Core Web Vitals, but the Long Animation Frames API reveals frames over 50ms, which are always visible as stutter. Collect them during known animations (for example, after opening a menu) along with their script attribution. A drop in long animation frames during interactions after an animation rewrite confirms the change helped real users, not just the lab profile.

Animation Libraries and Frameworks

Animation libraries differ widely in how they drive motion. Libraries built on the Web Animations API or CSS (and many modern React and Vue animation libraries in their default modes) hand transform and opacity animations to the browser, which can then composite them. Libraries that compute values in JavaScript every frame and write inline styles — common for spring physics and gesture-driven motion — keep the main thread busy and stutter when it is blocked. Check what your library does with a quick trace: if every animation frame shows scripting and style recalculation for the animated element, it is main-thread driven. Many such libraries offer a hardware-accelerated or WAAPI mode for simple transitions; use it for UI state changes and reserve per-frame JavaScript for interactions that truly need physics.

Framework transitions on mount and unmount (such as enter and leave transitions in Vue, or animated presence components in React) frequently animate height for collapse effects. Replace them with transform and opacity where the design allows, or contain the animated area so that its layout cost stays local.

A Rollout Plan

  1. Inventory animations on key templates with the Animations panel and code search.
  2. Rewrite layout-property animations to transform and opacity.
  3. Remove blanket will-change rules; add targeted, temporary ones where first-frame hitches appear.
  4. Replace scroll listeners for visual effects with scroll-driven animations.
  5. Add prefers-reduced-motion handling everywhere.
  6. Verify frames under throttling and check CLS and LCP in the field.

Common Pitfalls

  • Animating layout properties. top, left, width, height and margins run layout every frame.
  • will-change everywhere. Layer explosion and wasted memory.
  • Fading in the hero. Delays LCP.
  • JavaScript loops writing styles. Main-thread bound and vulnerable to long tasks.
  • Ignoring reduced motion. Accessibility and performance both suffer.

FAQ

Which CSS properties are cheapest to animate?

transform and opacity. On an element with its own compositing layer, they can be animated by the compositor without layout or paint.

Does will-change make animations faster?

It promotes an element to its own layer ahead of time, avoiding a hitch when the animation starts. It does not make layout-property animations cheaper, and overuse wastes memory.

Are CSS animations faster than JavaScript animations?

CSS animations and the Web Animations API can run on the compositor for transform and opacity. JavaScript loops that set styles every frame run on the main thread and are vulnerable to jank.

Do animations affect CLS?

Animations that move elements by changing layout properties can produce layout shifts. Transform-based movement does not count as a layout shift.

How do I animate height smoothly?

Height animation always involves layout. Options include animating grid-template-rows from 0fr to 1fr with containment, interpolate-size: allow-keywords where supported, or a transform-based reveal.

Why does my animation stutter only on the first run?

Often because a layer is created and painted when the animation starts. Promote the element shortly before animating with will-change, then remove it afterwards.

Do animations run in background tabs?

CSS animations and requestAnimationFrame are paused or throttled in background tabs. Timers with setInterval may still run, throttled.

Guides in This Topic

/html>