Web Platform · Rendering Performance
Compositing and Layer Promotion: When an Element Gets Its Own Layer
The rules that put an element on its own compositor layer, the memory each layer costs, and why promoting more elements can make a page slower instead of smoother.
Compositing exists so that some kinds of movement do not require the main thread. If an element is on its own layer, the compositor can translate, rotate, or fade that layer by rewriting a transform matrix and blending the result, without re-running style, layout, or paint. Everything about layer promotion follows from that: the browser promotes an element when it predicts that independent movement is worth the cost of an extra surface, and the cost is memory, raster work, and draw calls.
The layer tree is not the stacking context tree
A stacking context is a CSS concept that determines paint order and z-index resolution. A composited layer is an engine concept: a separate surface, usually a GPU texture, with its own transform. The two trees are related but not identical, and the differences matter.
The engine builds an internal paint-order tree in which nodes represent paint chunks, and then decides which chunks can be promoted. Promotion is constrained by paint order: if a promoted element has a non-promoted sibling painted above it, the engine may have to promote that sibling too, or “squash” the two into one layer, so that the composited result matches what the paint order requires. Squashing is the reason an element with will-change: transform can fail to get a layer of its own: the neighbours make independent compositing incorrect or wasteful, and the engine collapses them.
The conditions that trigger promotion
Engines decide promotion from a small set of signals:
- a transform with a Z component —
translateZ,translate3d,rotate3d,perspective— which cannot be expressed in the plane the page is painted on; - an animation of
transformoropacitythat the compositor can run; will-change: transformorwill-change: opacity, which is an explicit request;- video, canvas, and plugin content, which are separate surfaces already;
position: fixedand, in many cases,position: sticky, which must stay in place relative to the viewport or scroll container;- effects that require an offscreen buffer, such as
filter,backdrop-filter,mix-blend-mode, andopacitybelow 1 on an element with descendants; - overlap with an existing composited layer, when the engine chooses to promote rather than occlude;
- an iframe, which is cross-document content and gets its own layer.
Each of these is a prediction, not a guarantee. The heuristic set changes between engine versions, and it depends on the device: a transform animation may be composited on one platform and not on another, because the decision trades memory against main-thread work and those costs differ.
The memory arithmetic behind “do not promote everything”
A composited layer is backed by tiles, and tiles are bitmaps. At four bytes per pixel, a 512×512 tile is 1,048,576 bytes, about 1 MiB. A full-viewport layer on a 1920×1080 display covers about 2.07 million pixels, which at four bytes per pixel is roughly 8 MiB, before any overdraw, and the compositor may keep more than one copy while a frame is in flight or while a newly rastered tile replaces an old one.
That arithmetic is the entire argument against blanket promotion. A list of two hundred rows, each promoted, is two hundred surfaces whose combined area can exceed the viewport many times over. On a device with a shared memory architecture, that memory competes with everything else in the process; on a discrete GPU, it competes for VRAM and can force the driver to evict textures, which is a slow operation that shows up as a stall in the compositor rather than in script.
Promotion also multiplies raster work. Each tile must be rastered when its contents change, and content in a layer that overlaps other content cannot always be occluded away, so overlapping layers raster more pixels than the union of their areas would suggest.
Scale is not free even when transform is
A composited transform animation avoids layout and paint because the layer’s bitmap is reused and transformed. A scale animation is the exception in spirit: if the layer is scaled up, its existing bitmap is magnified, and the result is blurry. Engines respond by re-rastering at the new scale, either during the animation or at its end. Rastering at a larger scale costs proportionally more pixels, so a scale from 1 to 3 requires up to nine times the tile area at the end state.
The same applies to a static transform: scale(2) on a promoted element: the compositor may keep the layer at the original raster scale and let the GPU magnify it, which is acceptable for a solid colour and visibly wrong for text. Engines generally re-raster text at the composited scale to keep it legible, which is why scaling a large text block stays expensive even though it is “only a transform”.
Rotation has the same property in a different form: a rotated layer’s axis-aligned bounding box is larger than the original, and the tiles covering it must be rastered at sufficient resolution. At 45 degrees the box of a square element is twice the area of the square; the factor grows with the aspect ratio, so a 1000×100 element turned 45 degrees occupies a box about 778 pixels on each side, roughly six times its original area.
will-change is a lifetime, not a one-shot hint
will-change: transform tells the engine that the element’s transform is expected to change. Engines honour it by promoting the element ahead of the change, which avoids the first-frame hitch that would otherwise occur when an animation starts. The cost is that the layer and its memory exist for as long as the declaration applies — typically the entire lifetime of the element, including while it is idle.
The pattern that avoids the permanent cost is to add the declaration shortly before the animation and remove it afterwards, or to rely on the fact that an actively animating transform is promoted without the hint and omit will-change entirely. The latter is usually correct: the engine already knows the transform is changing because it is animating it.
/* Promoted for the whole lifetime of the element. */
.panel { will-change: transform; }
/* Promoted only while the class is present. */
.panel.is-dragging {
will-change: transform;
transform: translate3d(0, var(--drag-y), 0);
}
will-change on a property that is not compositable, such as width or top, does not help: the engine cannot move that work off the main thread, and the declaration only adds bookkeeping. The list of properties that can be handled by the compositor is short — transform, opacity, and, with caveats, filter and backdrop-filter.
Effects that force an offscreen buffer
opacity below 1, filter, backdrop-filter, and mix-blend-mode require the element and its descendants to be rendered into a separate buffer and then blended. That buffer is a layer whether or not the element animates, and the blending step reads both the buffer and what is behind it. A backdrop-filter in particular must sample the content behind the element, which means the compositor cannot simply draw the element over a cached background; it must have the backdrop available as a texture.
Animating opacity on such an element still runs on the compositor, but the per-frame cost includes re-blending a full layer, not just updating a matrix. On a large element that covers the viewport, the difference between an opacity animation on a plain layer and one on a backdrop-filtered layer is the difference between a matrix update and a screen-sized blend.
Layer explosion, and how it presents
The failure mode is not a crash. It presents as jank that gets worse as content is added, a compositor thread that is busy while the main thread is idle, high memory use that grows with the number of promoted elements, and raster work that never seems to finish on scroll. Because the cost is on the compositor and raster threads rather than in script, a profile of JavaScript shows nothing.
The diagnostic direction is the reverse of the usual one: if frames drop while the main thread is idle, and the page contains many promoted elements or large blurred surfaces, the compositor is the suspect. Reducing promotion — removing will-change, replacing filter with a pre-rendered image, avoiding backdrop-filter on large areas, and letting engines decide when to promote — is the fix.
The interaction with scrolling is important enough to treat separately: compositor-driven scroll depends on layers staying stable, and creating or destroying layers during scroll forces re-raster at the worst moment. Scrolling and the Compositor Thread covers that relationship, and Painting, Tiles, and Raster Cost covers what happens inside a layer when its content does change.
References
- CSS Transforms Module Level 2 — W3C
- CSS Compositing and Blending Level 1 — W3C
- CSS Will Change Module Level 1 — CSS Working Group