TL;DR / Layout computes geometry, paint fills pixels, and composite assembles layers — each phase is progressively cheaper, so the best animations only trigger compositing.
How It Works
Rendering phases (cost: high to low)
┌──────────────┐
│ Layout │ width, height, position (EXPENSIVE)
└──────────────┘
│
triggers
↓
┌──────────────┐
│ Paint │ color, shadow, background (MEDIUM)
└──────────────┘
│
triggers
↓
┌──────────────┐
│ Composite │ transform, opacity (CHEAP)
└──────────────┘
The browser's rendering pipeline has three main phases after the render tree is constructed. Understanding which phase your CSS change triggers determines whether your animation runs at 60fps or at 6fps. The phases form a cascade — triggering an earlier phase forces all subsequent phases to run.
Layout (Reflow)
Layout computes the exact size and position of every element on the page. It evaluates the box model, runs flexbox and grid algorithms, resolves percentage-based dimensions, and determines how elements flow relative to each other.
Properties that trigger layout: width, height, padding, margin, border, top, left, right, bottom, font-size, line-height, display, position, flex, grid-template-*, text-align, and many more. Essentially, any property that could change where elements are positioned or how large they are triggers layout.
Layout is the most expensive phase because it is global — changing one element's size can cascade through the entire document. A parent that shrinks may cause children to wrap, which affects siblings, which affects ancestors. The browser must resolve these dependencies, often requiring multiple passes through the tree.
Paint
After layout is complete, the browser walks the render tree and records paint operations — the actual drawing commands for filling pixels. This includes text rendering, background colors, images, borders, shadows, and border-radius clipping.
Properties that trigger paint (but not layout): color, background-color, background-image, box-shadow, text-shadow, border-style, border-color, outline, visibility, border-radius.
Paint is cheaper than layout because it does not require recalculating positions, but it can still be expensive for complex visual effects — large box shadows, blurred backgrounds, or elements with many draw operations. The painted output is stored as a bitmap for each compositing layer.
Composite
The compositor takes the painted layer bitmaps, applies transforms (translate, rotate, scale) and opacity changes, and assembles them into the final frame. Compositing runs on the GPU, often on a separate thread from the main thread.
Properties that trigger only compositing: transform and opacity. These are special because the GPU can manipulate the already-painted bitmap without involving the main thread at all. The element's pixels were already painted; the GPU just moves, rotates, scales, or fades the bitmap.
This is why transform: translateX(100px) is orders of magnitude cheaper than left: 100px. Both visually move an element, but left triggers layout (repositioning all affected elements) and paint (redrawing the element at its new position), while transform only adjusts the layer's GPU position.
The Cascade Effect
The phases are strictly ordered: layout triggers paint which triggers composite. You cannot skip a phase in the middle. If you trigger layout, paint and composite will also run. If you trigger paint, composite will also run. But if you trigger only composite, layout and paint do not run.
This cascade is why CSS animation performance guides consistently recommend:
- Best: animate
transformandopacityonly — composite-only, GPU-driven - Acceptable: animate paint-only properties — triggers repaint of affected layers
- Worst: animate layout-triggering properties — recalculates the entire layout tree
Practical Performance Impact
A layout change that touches 500 elements can take 10-20ms. A paint operation for a complex layer might take 5ms. A composite operation is typically sub-millisecond. At 60fps, your total budget per frame is 16.6ms. Layout-triggering animations quickly consume this budget; composite-only animations barely register.
Measuring Phase Costs
Chrome DevTools' Performance tab records each phase with precise timing. Enable "Paint flashing" in the Rendering tab to visually highlight elements that repaint. The "Layers" panel shows which elements have their own compositing layers and why.
The web.dev documentation on rendering performance catalogs which CSS properties trigger which phases. Cross-referencing the MDN page for each property you plan to animate helps verify whether it triggers layout, paint, or only compositing.
The contain Property
CSS contain: layout paint tells the browser that changes inside the contained element cannot affect elements outside it. This allows the browser to scope layout and paint to the subtree, reducing the cost of both phases significantly.
Gotchas
transform: translateZ(0)is a hack, not a solution. It promotes an element to a compositing layer, but if the animation still triggers layout (e.g., you also animatewidth), the promotion only adds memory overhead without improving performance.- Paint complexity is not just about pixel area. A small element with a 100px
blur()filter or complexclip-pathcan be more expensive to paint than a large simple rectangle. will-changedoes not skip phases. It promotes an element to its own layer (allowing cheap compositing), but if you then changewidth, layout still runs on the main thread before the layer is repainted.- SVG and canvas have their own rendering pipelines. SVG elements participate in the normal layout/paint/composite flow, but canvas bypasses layout and paint — drawing commands execute directly on the bitmap. This makes canvas faster for dynamic graphics but removes it from the CSS styling system.
- Changing
z-indexalone does not trigger layout — it only affects paint order and compositing. But changingz-indexon an element that overlaps a composited layer can trigger implicit layer creation, which has its own costs.