Web Platform · Rendering Performance
Painting, Tiles, and Raster Cost: What a Repaint Actually Recomputes
Display lists, invalidation rectangles, and tile rasterization: which changes force re-recording, which force re-rastering, and why blur and text dominate the bill.
A repaint is not the act of drawing pixels again. It is the act of re-recording a list of drawing commands and then, separately, executing those commands into bitmaps. Splitting the two makes the pipeline parallel and makes partial updates possible, and it also means that the cost of a visual change depends on two different quantities: how much of the display list must be rebuilt, and how many pixels must be rastered at what scale.
Display lists are recorded, not drawn
For each paint layer, the engine walks the box tree in paint order and emits a flat list of display items: rectangles with fills and borders, glyph runs with their font and offset, images with source rectangles and destination rectangles, shadow and blur operations, and clip and transform stack operations. Each item carries geometry in the layer’s coordinate space.
Because the list is data, several things become possible that a direct-to-bitmap painter cannot do. Items can be culled before rasterization if their bounds fall outside the visible area. Items can be replayed at a different scale for pinch-zoom. Items can be discarded and rebuilt for a small region while the rest of the list is reused.
The list is also immutable in the common case: a style change that does not affect geometry still produces a new list for the affected paint layer, because the items’ visual parameters changed. This is why background-color changes are cheap in layout terms and still not free.
Invalidations are rectangles, and rectangles merge badly
Paint invalidation starts as a rectangle around the changed element’s painted bounds, expanded to include effects that extend beyond the border box: box shadows, outlines, and blur radii all enlarge it. The engine then walks up the paint-order tree and marks every ancestor’s display list as needing to be rebuilt in the intersected region.
The merging is the interesting part. Disjoint invalidations that hit the same display list are usually accumulated rather than merged into one bounding box, because a bounding box around two distant changes would force re-recording everything between them. But each invalidated region still costs a rebuild of the items intersecting it, and if the number of small invalidations grows, the per-region overhead can exceed the cost of one large rebuild. An animation that changes a hundred scattered elements per frame is a different workload from one that changes one large element, and neither is obviously cheaper.
Tiles bound the raster work
Raster happens per tile. A tile is a fixed-size bitmap covering a rectangular piece of a layer, commonly 256×256 or 512×512 pixels. A 512×512 tile at four bytes per pixel is 1,048,576 bytes; the same tile at a device pixel ratio of 2 covers 256×256 CSS pixels and still occupies 1 MiB, so a page that looks the same at twice the density rasteres four times the pixels.
Tiles give the engine a granularity for invalidation: touching one pixel in a tile requires rastering the whole tile, and touching one pixel in each of a thousand tiles requires rastering a thousand tiles. A large element with a small animated child therefore has a fixed raster cost set by the tile size, not by the child’s area. This is the mechanism behind the advice to keep animated content in its own layer: the alternative is re-rastering tiles that mostly contain static pixels.
The engine also decides raster scale per tile. Content that is stable and not scaled can be rastered at the current device scale; content inside a scale transform may be rastered at a higher scale to stay sharp. A tile rastered at twice the linear scale uses four times the memory, so scale is not a free quality knob.
What makes a single paint operation expensive
Three properties dominate per-pixel raster cost.
Area. Filling a rectangle is proportional to its pixel count. Replacing a small icon with a full-screen gradient changes a cheap operation into a screen-sized one.
Blur. A Gaussian blur with radius r samples approximately (2r+1)² source pixels per output pixel in a naive implementation; separable passes plus downsampling bring that to roughly linear in r per output pixel, which still leaves a cost proportional to both the blurred area and the radius. A large radius on a large element is worse than either factor alone. A box-shadow with a large blur radius, or a filter: blur() on a large element, is among the most expensive things a page can paint. Shadows are specified as blurred copies of the border box, and the blur radius extends the painted area in every direction, so a shadow on an element also increases the invalidation rectangle.
Text and complex shapes. Glyph runs must be rastered with hinting and subpixel positioning considerations, and the cost per glyph is far higher than a rectangle of the same area. Border radii, which require antialiased coverage calculations on the curved edges, cost more than square corners. Clipping and masking add per-pixel work and, in the case of masks, an extra texture fetch.
/* Cheap: a solid fill over a small area. */
.badge { background: #2b6cb0; border-radius: 999px; }
/* Expensive: a wide blur whose sampled area is large in both axes. */
.hero {
box-shadow: 0 0 80px 20px rgb(0 0 0 / 0.35);
filter: blur(2px);
}
The second rule’s cost is not the visible shape but the thousands of pixels sampled around it, multiplied by the area of the element.
Overdraw and occlusion
The compositor draws layers in order, and content hidden behind opaque content still costs if it is drawn first. Engines attempt occlusion culling: if a fully opaque element covers a region, items painted earlier within the same layer can be skipped, and entire layers can be skipped if an opaque layer above them covers their bounds. The optimisation is conservative — it requires proof of opacity and of the absence of blending — so a background-color with slight transparency, or a mix-blend-mode, disables culling for everything beneath.
Overdraw is the reason full-screen fixed backgrounds are more expensive than they appear: the background is painted, then the content above it is painted over it, and the covered pixels were wasted. It is also why video and canvas elements, which are separate surfaces, can be cheap to composite but expensive when a translucent overlay covers them, because the blend requires reading both surfaces.
Scrolling changes the invalidation pattern
Once a layer is rastered, scrolling it within a compositor-scrolled container costs no raster at all: the tiles are reused and the compositor translates them. That is the point of compositor-driven scrolling. The exception is content that is fixed or sticky relative to the viewport while its containing layer scrolls; such elements must be repositioned relative to the scroll offset, and depending on the engine’s layer assignment, that can mean re-rastering or promoting them into their own layer.
Elements that change their appearance as they scroll — a header that shrinks, a shadow that appears — force paint work on each scroll frame unless the effect is expressed as a compositor-friendly change. A box shadow that fades in is a paint animation; an opacity fade of a pre-rastered shadow layer is a compositor animation. The difference between them is entirely in which thread runs per frame.
Reading paint problems from a recording
Paint flashing in developer tools highlights the regions the engine re-records. It shows invalidation area, not raster cost, and the two can diverge sharply: a large low-complexity invalidation may be cheaper than a small blurred one. The complementary view is the layer and tile view, which shows how many tiles a layer is divided into and which of them are being rastered.
A useful rule of thumb for triage: if the recording is small but frames are slow during scroll, the cost is raster or compositing, not paint. If the recording covers the viewport on every frame, the change is escaping containment and the fix belongs one stage earlier, as described in Layout Invalidation and Dirty Bits. If re-recording is rare but frames still drop, the likely cause is a long task elsewhere on the main thread, covered in Frame Budgets, Long Tasks, and Input Latency.
One more boundary: paint work for offscreen content. Content outside the viewport that remains in the layout tree can still be recorded into display lists, and, when a layer covers a long scrollable area, tiles for offscreen regions are typically not rastered until they approach the viewport. That deferral is what makes long documents scrollable at all, and it is the mechanism that content-visibility extends to style and layout, as described in CSS Containment and Content Visibility.