Skip to content
Browser Lab · LAB-01Browser Rendering

Compositing and the Layer Economy

Compositor layers make animation cheap and memory expensive. This dossier documents what promotes an element to its own layer, what that buys, and where the practice of promoting everything goes wrong.

Level
RESEARCHER
Status
ACTIVE
Experiments
09
Observations
18

Question

Why can a transform animation stay at 60fps while the main thread is completely blocked, and why does adding will-change: transform to a list of 500 rows make the page slower?

Background

After paint, the page is a set of display lists. The compositor turns those into textures and assembles them into a frame. Content that lives in its own layer can be moved, scaled or faded by changing a transform matrix on the GPU, with no repaint and no main-thread involvement.

OperationWhere it runsMain thread needed
transform / opacity on a composited layerCompositor threadNo
scroll (in the common case)Compositor threadNo
background-color changeMain thread paintYes
width / height changeMain thread layout + paintYes
non-passive touch/wheel listenerForces main-thread scrollYes

Findings

Finding 1. Every layer costs memory. A layer's texture is roughly width × height × device pixel ratio² × 4 bytes. A full-screen layer on a 3× phone is several megabytes. Hundreds of layers exhaust GPU memory and force the compositor into slower paths.

Finding 2. Promotion is not free at creation time. Promoting an element requires a separate raster pass, and un-promoting it requires the content to be re-rastered into its parent. Toggling will-change on hover can cost more than the animation saves.

Finding 3. Layer explosion is usually accidental. An element promoted for animation forces overlapping elements above it to be promoted too, so the compositor can preserve paint order. One bad promotion can cascade.

Finding 4. Scrolling is compositing. Non-passive wheel and touch listeners force scrolling back onto the main thread, because the browser must wait to learn whether the event will be cancelled. { passive: true } is the fix, and it is the default for wheel and touch listeners on the document.

Method

  1. 01Enable Layer borders and the Layers panel in Chrome DevTools.
  2. 02Record a trace while the animation runs; check whether frames appear on the compositor thread only.
  3. 03Count layers and read the reported memory before and after a change.
  4. 04Apply will-change immediately before the animation and remove it after, rather than declaring it statically.
example.jsjs
el.style.willChange = "transform";requestAnimationFrame(() => { el.animate([{ transform: "translateX(0)" }, { transform: "translateX(240px)" }], { duration: 300, easing: "ease-out" }) .finished.then(() => { el.style.willChange = "auto"; });});
Promote late, demote early

Limitations

References

  1. 01web.devStick to compositor-only properties
  2. 02MDNwill-change
  3. 03MDNImproving scroll performance with passive listeners