Reduce Repaints from Animated CSS Properties

Unnecessary repaints during CSS animations degrade frame rates, consume excess battery, and introduce visible jank across devices. This article outlines the primary strategies web engineers use to eliminate redundant paint cycles, focusing on compositor-only properties, layer promotion techniques, CSS containment, and performance profiling workflows designed to maintain a consistent 60 or 120 frames per second.

Stick to Transform and Opacity

Browsers render updates through three sequential stages: Layout, Paint, and Composite. Animating geometric properties such as width, height, top, left, or margin forces the browser to recalculate element geometry (layout) and redraw pixels across affected surfaces (paint). Animating visual properties like color or background-color skips layout but still forces a repaint on every frame.

To bypass both layout and paint entirely, animate only transform (including translate, scale, and rotate) and opacity. These properties are handled entirely by the compositor thread on the GPU, allowing visual changes to occur without requiring the CPU to rasterize new pixels.

Promote Animated Elements with will-change

When an element animates on the standard render tree, its movement can dirty the entire layer, forcing surrounding elements to repaint. Promoting the moving element to its own compositor layer isolates its visual changes.

Apply the will-change property to signal this requirement to the browser in advance:

.animated-box {
  will-change: transform;
}

Use will-change deliberately and sparingly. Creating too many compositor layers consumes substantial GPU memory and introduces compositor overhead that can paradoxically reduce performance. Remove or reset the property when an animation concludes.

Isolate DOM Subtrees Using CSS contain

The contain property informs the browser that an element's internal styling and layout do not impact the rest of the document. Applying containment prevents repaint events inside a container from triggering cascading repaints outside of it:

.card-container {
  contain: paint;
}

Using contain: paint ensures that descendant elements cannot draw outside the container bounds. If an offscreen or clipped child element repaints, the browser ignores rendering passes outside the designated bounding box.

Avoid Inadvertent Layout Thrashing

CSS animations interacting with JavaScript-driven logic often trigger unintentional repaints through layout thrashing. If script logic reads layout metrics (such as getBoundingClientRect(), offsetWidth, or scrollTop) immediately after mutating styles, the browser is forced to run synchronous layout and repaint passes mid-frame.

To prevent this, batch all DOM style mutations separately from DOM read operations, or rely on requestAnimationFrame() to sync visual updates with browser refresh cycles.

Audit Paint Rectangles Using Browser DevTools

Identifying rogue repaints requires continuous runtime profiling:

  1. Open DevTools in Chrome or Chromium-based browsers.
  2. Open the Rendering tab via the More tools menu.
  3. Enable Paint Flashing.

With Paint Flashing active, any repainted region flashes green on screen. A well-optimized animation should show zero green flashes during movement; the animated element should move smoothly across the screen without triggering any repaint borders.