Part 4 · 5 chapters · ~35 min

Paint And Compositing Algorithms

Paint order is a tree walk in seven phases, the display list is what the walk records, culling makes raster depend on the visible area, tiles and damage decide what is redrawn, and the compositor is a sorted draw of textured quads. This part is each as an algorithm, with the cost formula that follows: damage area times overdraw times the dearest effect, per layer.

25

Paint order: a tree walk in seven phases

the question

"z-index: 9999 and it is still underneath. What is the algorithm that decides?"

A depth-first walk of the layout tree in which every stacking context is painted as one unit, in seven phases, with z-index ordering only the positioned children of a single context. An element can never paint above something that is above its stacking context's root, because the context is painted whole, at its root's position in the parent's order. The 9999 is ordering the element among its siblings in a context that is itself underneath.

the phases, per stacking context
  1. The context root's background and borders.
  2. Child stacking contexts with negative z-index, in ascending order.
  3. In-flow, non-positioned, block-level descendants' backgrounds and borders (not their content), in tree order.
  4. Floats.
  5. In-flow inline content (text, inline backgrounds, replaced elements), in tree order.
  6. Positioned descendants with z-index: auto or 0 (as contexts if they are contexts, else as if they were), in tree order.
  7. Child stacking contexts with positive z-index, in ascending order.
what the algorithm implies
  1. z-index is relative to the nearest ancestor stacking context. Raising it moves the element among that context's children; it cannot escape the context.
  2. Creating a context flattens the subtree's z-indexes relative to the outside: adding opacity: 0.99 or a transform to a wrapper makes its children's z-indexes irrelevant to anything outside the wrapper. The common accidental cause of "my dropdown is under the next card".
  3. Phase order beats tree order: a positioned element with z-index 0 (phase 6) paints over later in-flow siblings (phase 3 and 5) regardless of DOM order; a float (phase 4) over block backgrounds (3) but under inline content (5), which is why text wraps around a float and paints on top of its background if they overlap.
  4. The compositor's layer candidates are the same list. An element that creates a stacking context is a unit; units are what can be lifted to layers (the browser course, part 5). isolation: isolate creates a context with no other effect, and is the clean way to scope z-index.
the sizes
The walk is O(elements) and happens once per paint; it is never the bottleneck. Its cost is in what it produces: a display list whose length and overdraw decide raster time (next two chapters).
PAINT ORDER AS A TREE WALK
stacking contexts, z-index, and the seven phases
swipe the figure sideways, or tap expand for full screen
1/6
the tree
The tree: body > [header (position: relative; z-index: 2), main > [article, aside (position: absolute; z-index: 1)], footer]. Stacking contexts are created by the root, by header (positioned with z-index), and by aside. main and article are not contexts: their children paint as part of the root's context.
26

Display lists and culling

The paint walk does not draw; it records. The output is a display list: a flat sequence of drawing commands with bounds. Separating recording from rasterisation lets the engine rasterise on other threads, re-rasterise only what changed, and skip commands that cannot affect the pixels being produced. Skipping is culling, and with a spatial index it makes raster cost depend on what is visible rather than on how long the page is.

the list
  1. Commands: draw rect, draw rounded rect, draw text run (glyphs and positions), draw image, draw path, save/restore with clip or transform, and nested sub-lists (a picture referencing another picture, used for repeated content and for layers).
  2. Bounds: each command carries its device-space bounding rect, computed at record time. For a text run, the union of glyph bounds; for a shadow, the rect plus the blur radius.
  3. Chunks: consecutive commands sharing a layer, transform, clip and effect are grouped. A chunk is the unit assigned to a compositor layer's tiles.
  4. Size: a few hundred bytes per command; a complex page has tens of thousands. Skia's recorded pictures are the implementation in Chrome.
culling
  1. Bounds culling: when rasterising a tile, skip every command whose bounds do not intersect the tile. Four compares per command; with an R-tree over bounds, O(log n + k) per tile instead of O(n).
  2. Occlusion culling: an opaque command that fully covers an earlier command's bounds (within the tile) makes the earlier one invisible; skip it. Done conservatively for axis-aligned opaque rects; a solid page background under a full-size opaque element lets the engine skip the background for those tiles.
  3. Clip culling: commands entirely outside the current clip are skipped; a overflow: hidden container with 10,000 children offscreen costs recording but not raster.
  4. Empty-effect elision: transparent fills, zero-size rects, and text runs with no glyphs are dropped at record time.
what is not culled
  1. Recording is O(elements) regardless; a very long page costs its paint walk even if only the top is visible. (content-visibility: auto skips the recording of offscreen subtrees too.)
  2. Overdraw: commands that survive culling and overlap each other each draw their pixels; a background under a semi-transparent panel under text is three writes per pixel.
  3. Expensive effects: blur, backdrop-filter and large shadows are per-pixel costs that culling reduces only by area, not by the per-pixel factor.
DISPLAY LIST CULLING
skipping what cannot be seen
swipe the figure sideways, or tap expand for full screen
1/6
the list
A display list for a page: 2,000 commands with precomputed bounds (each a rect in layer space). The viewport is 1,200 × 800 at scroll 0; the page is 4,000 tall. Rasterisation happens per tile (256 × 256) for the visible region plus a margin.
27

Tiling, damage, and what gets redrawn

A layer's content is rasterised into fixed-size tiles so that a change can re-rasterise a small region and a scroll can show already-rastered regions immediately. The region to re-rasterise is the damage: the union of the old and new bounds of everything that changed. The entire economics of repaint follows from the damage rect's size and which layer it lands on.

tiles
  1. Size: 256 × 256 or 512 × 512 device pixels, chosen for GPU texture efficiency and upload granularity. A 1,200 × 2,400 layer is about 50 tiles.
  2. Priority: tiles are rastered in order of distance from the viewport (visible first, then the scroll-direction margin), on a pool of raster threads. A tile's work is: replay the layer's display list with the tile's clip and culling (chapter 2), into a bitmap, then upload to a GPU texture (or raster directly on the GPU).
  3. Fallback tiling: a low-resolution copy of the layer (one-eighth scale) so that fast scrolls into unrastered regions show blurry content rather than nothing (checkerboard).
  4. Memory: each tile is width × height × 4 bytes; 50 tiles at 256² is 13 MB. A page with many large layers runs into the tile memory budget and starts evicting offscreen tiles, which then re-raster on scroll.
damage
  1. Computed from invalidations: each changed display item contributes its old bounds and its new bounds. Style changes that do not move anything damage one item's rect; layout changes that move content damage every moved item.
  2. Tiles intersecting the damage are invalidated and re-rastered; others keep their bitmaps.
  3. Zero damage: a transform or opacity change on a compositor layer. The content is identical; the compositor redraws the existing tiles with the new matrix. This, and only this, is why those properties animate at 60 fps for free.
  4. Whole-layer damage: a layout shift near the top of a layer, a font swap, a theme toggle. Every tile re-rasters. On a long page, that is the full page's raster even though only a screen is visible, because offscreen tiles in the margin are also invalid and will be needed on the next scroll.
code
// the cost of a raster, approximately: pixels × (commands touching them) × (per-pixel cost of each)
// solid fill:      ~0.1 ns/pixel (memset-like)          text:        ~1 to 5 ns/pixel (glyph blits from the atlas; AA)
// image blit:      ~0.5 ns/pixel (scaled: more)          gradient:    ~1 to 2 ns/pixel
// box-shadow blur: ~10 to 50 ns/pixel (a separable Gaussian: O(radius) per pixel per pass)
// filter: blur()   same, over the whole element and its margin     backdrop-filter: that, plus reading back what is beneath
// border-radius:   AA edges are a thin band; the interior is a fill
// a 1,200 × 800 viewport is ~1M pixels: a solid repaint is ~0.1 ms; the same with a 20px blur under it is ~20 ms

// so the repaint cost is dominated by: area × overdraw × the dearest effect in the stack. the Rendering panel's paint flashing
// shows area; GPU overdraw tools show the multiplier; the effect is in your CSS. moving a blurred element to its own layer
// turns per-frame raster into a one-time raster plus compositing (the browser course part 5), at the cost of its texture memory.
the rule
The cost of a visual change is (damage area) × (overdraw) × (per-pixel cost of the effects in that area), on each layer it touches. Small, isolated, effect-free damage is free; a shift that moves a blurred panel is the worst case.
TILING AND DAMAGE
what gets re-rasterised when one thing changes
swipe the figure sideways, or tap expand for full screen
1/6
tiles
A layer 1,200 × 2,400, tiled at 256 × 256 (plus a low-resolution fallback tiling for fast scrolls). 5 × 10 = 50 tiles; the visible region covers 5 × 4. Each tile is a GPU texture holding its rasterised pixels.
28

Compositing: layers as a sorted draw

the compositor's algorithm per frame
  1. Inputs: a tree of layers, each with its tiles (textures), transform, opacity, clip, and scroll offset; plus the latest input (scroll deltas, pinch) applied on the compositor thread without the main thread.
  2. Property trees: transforms, clips, effects and scroll offsets are stored in separate trees (not on the layer tree) so that a change to one node updates descendants by tree walk, and so that layers can be sorted independently of their DOM nesting.
  3. Draw order: layers are sorted by their paint-order position (chapter 1), with 3D transforms handled by sorting contexts (a transform-style: preserve-3d subtree is sorted by depth). The sort is O(layers log layers); layer counts are tens to hundreds.
  4. Quads: each visible tile becomes a textured quad with its layer's transform and opacity. Occlusion between opaque layers removes quads fully covered by opaque quads drawn above them (the same idea as display list occlusion, at the layer level).
  5. Draw: the quads are submitted to the GPU in one or a few draw calls per render pass. Effects that need intermediate textures (opacity groups with multiple children, filters, masks, blend modes) create render passes: draw the subtree to a texture, then draw that texture with the effect. Each pass is a full-size texture and a copy.
  6. Present: the frame is handed to the display at the next vsync.
costs and their causes
  1. Layer count: each layer is textures (memory) and quads (draw work). Hundreds of layers from will-change: transform on every list item is a known failure: tile memory exhausted, and the sort and draw overhead per frame.
  2. Render passes: an opacity below 1 on a group with overlapping children, a filter, a mix-blend-mode, a mask: each forces an offscreen pass over the group's area. Nested ones multiply. An animated filter: blur() is a full-area offscreen blur per frame.
  3. Overlap: a composited layer that overlaps non-composited content forces the overlapped content into its own layer too (so draw order can be preserved), which is how one transform can create many layers ("layer squashing" limits the damage by merging overlapped content into one layer where possible).
  4. Texture uploads: re-rastered tiles must be uploaded; on integrated GPUs the upload bandwidth is the limit, and a full-layer damage on a 4K display is tens of megabytes per frame.
the sizes
A healthy page: 10 to 50 layers, 1 to 3 render passes, a few hundred quads. The Layers panel shows the count and each layer's compositing reason; "Frame Rendering Stats" in Rendering shows the frame time; a jump when a filter animates is a render pass per frame.
29

Measuring paint and composite

QuestionToolWhat to read
What repainted on this change?Rendering → Paint flashingThe damage rect, lit green; a whole-layer flash on a small change means a layout shift or a layer-wide invalidation
How long did paint and raster take?Performance → Paint and Rasterize Paint events (main and raster threads)Paint (recording) time versus raster (pixel) time; raster dominated by area and effects
How many layers and why?Layers panel (More tools)Each layer's size, memory, and compositing reason; hundreds of layers is a bug
Which layer did the change hit?Layers panel → paint profiler for the layerThe layer's display list and per-command cost
Is the animation compositor-only?Performance: a frame with no Paint, Layout or Recalculate Style; only CompositeTransform and opacity on their own layer; anything else is per-frame raster
Is overdraw the cost?Android GPU overdraw debugging (for WebViews); Skia debugger on a captured picture; reasoning from the stack of translucent layersPixels painted several times; stacked translucent backgrounds
Are tiles checkerboarding on scroll?Rendering → Frame Rendering Stats; chrome://tracing cc categoriesTiles not ready in time; raster behind scroll; too little margin or too much layer memory pressure
Is a render pass per frame?Performance with Advanced paint instrumentation; GPU laneFilters, masks, blend modes, group opacity on animated subtrees
the pointer
Part 5 is scheduling: the event loop's task selection, React's lane model as priority bitmasks, cooperative yielding against deadlines, and the Prioritized Task Scheduling API. Everything in this part happens inside one frame; the next part is deciding what runs in which frame.