sfw/fix
Non-composited animations medium

Avoid non-composited animations

An animation changes a property that forces layout or paint on the main thread instead of running on the compositor, causing jank.

What you see

Avoid non-composited animations
Animations which are not composited can be janky and increase CLS.
(Unsupported CSS property: width)

What’s actually happening

Lighthouse flags one or more elements and names the offending property — width, height, top, left, margin, or box-shadow. The animation looks fine on a fast laptop and stutters on a mid-range phone. The reason: those properties can't be handed to the compositor, so every frame re-runs layout or paint on the main thread, competing with your JavaScript. When the main thread is busy, frames drop, scrolling feels sticky, and INP and CLS take the hit.

Common causes

  • Animating geometric properties — width, height, top, left, right, bottom, margin, padding — which forces a layout recalculation on every frame
  • Animating paint-only properties like box-shadow, background-color, or color on large or numerous elements
  • JavaScript-driven animation (jQuery .animate(), or a rAF loop writing style.left) that mutates layout properties frame by frame
  • Transitioning a property like 'height: auto' or 'all', so the browser can't promote it to a compositor layer
  • A library or hover effect animating filter/clip-path on elements the compositor can't isolate

How to fix it

  1. Animate transform and opacity insteadReplace width/height/top/left with transform: translate() and scale(), and fade with opacity. These two are the only properties the browser can animate entirely on the compositor — the main thread stays free, so the work doesn't touch layout or paint. A slide becomes `transform: translateX(...)`, a size change becomes `transform: scaleY(...)`.
  2. Promote the layer when it genuinely helpsAdd `will-change: transform` (or `opacity`) to the animated element so the browser keeps it on its own layer. Use it sparingly — applying will-change to dozens of elements wastes memory and can backfire. Drop it once the animation is idle.
  3. Confirm in DevTools before and afterOpen the Performance panel, record the interaction, and look for purple 'Layout' and green 'Paint' bars during the animation. In the Rendering tab enable 'Paint flashing' — green flashes over the animated area mean it's still repainting. A clean transform animation shows neither.
  4. Fix JS-driven animations at the sourceSwap jQuery .animate() of position/size for a CSS class toggle that transitions transform, or use the Web Animations API targeting transform/opacity. If a rAF loop writes style.top each frame, change it to write style.transform instead.
  5. Avoid animating shadows and 'all'For a glow or lift effect, animate the opacity of a pseudo-element that already holds the box-shadow rather than the shadow itself. Never write `transition: all` — name the exact compositor-friendly properties so the browser doesn't try to animate layout.

Stop it recurring

Make transform and opacity the default vocabulary for any motion in the codebase, and reserve will-change for elements that are actually animating.

Related errors