skip to content

Paint and Compositing Layers

You will learn how painted output is split into layers and handed to the GPU, and why transform and opacity are the cheap properties. Interviewers use will-change and layer explosion to see if you know the cost side of the trick.

on this pageshow

questions

5

In a browser, why does changing an element's `transform: translateX()` normally skip layout and paint work, while changing its `left` does not?

level: middleimportance: must knowfreq 72%

answer

  1. which stage does the property enter
  2. one is layout, one is post-layout
  3. the texture already exists
  4. only the matrix changes
  5. true only once promoted

basics

~20 s

left is a layout property: changing it invalidates geometry, so the browser re-runs layout, repaints the affected content and recomposites. transform on a composited layer changes only the matrix the compositor uses to draw an already-rasterized texture, so layout and paint are skipped entirely.

solid answer

~50 s

They enter the pipeline at different stages. `left` participates in layout — it positions the box inside its containing block — so changing it invalidates layout, and everything downstream of layout must run again: paint the affected content, rasterize it, composite. `transform` is applied after layout; the box keeps the same laid-out geometry and nothing around it moves. If the element already has its own compositing layer, its content is a texture the compositor holds, and a new translation is just a different matrix passed to the GPU — no new paint recording and no re-raster. That is the whole reason `transform` and `opacity` are called compositor-friendly. The caveat is that it only holds when the element is actually promoted; on a non-promoted element the change still forces a repaint of the region it paints into.

code

css · 26 lines
css
.box {
  position: absolute;
  top: 0;
  left: 0;
  width: 120px;
  height: 120px;
  background: #0af;
}

/* invalidates layout on every frame */
.box.expensive {
  animation: move-left 1s linear infinite alternate;
}

/* composite-only on every frame */
.box.cheap {
  animation: move-transform 1s linear infinite alternate;
}

@keyframes move-left {
  to { left: 300px; }
}

@keyframes move-transform {
  to { transform: translateX(300px); }
}

go deeper

for a junior

Know that transform and opacity are the two properties described as compositor-friendly, and that properties like left, top, width and margin force the browser to redo layout.

for a middle

Be ready to derive the cost from the pipeline stage a property enters, and to name what is invalidated in each case: layout onward, paint onward, or the composite parameters only.

for a senior

Demonstrate the caveats — promotion is a precondition, scale can still force re-raster, and a layer whose contents change every frame repaints every frame no matter which property drives the motion.

for a principal

Own the fact that this is a mechanism, not a rule to apply blindly: leaning on transform everywhere trades main-thread work for GPU memory and layer-management overhead, and that trade has a different answer on a low-end phone than on a workstation.

## Two properties, two entry points into the pipeline The pipeline runs style, layout, paint, raster, composite. What makes a property cheap or expensive is simply how early it enters that chain — everything after the entry point must be redone. `left` (together with `top`, `width`, `height`, `margin`, `padding`, `font-size` and friends) is consumed by **layout**. The used value of `left` positions the box relative to its containing block, so changing it means the box's geometry is no longer valid. The browser marks it dirty, re-runs layout for at least that box, then repaints the region, rasterizes the new bitmap, and composites. `transform` is consumed **after layout**. The layout engine has already decided where the box lives and how big it is; a transform is a visual mapping applied to the painted result. Nothing else on the page moves because of it — in-flow siblings keep their positions, and text does not reflow around a translated box. So layout has nothing to redo. ## Why composite-only is nearly free The compositor keeps, per layer, a rasterized texture plus a small set of parameters: a transform matrix, an opacity, a clip. Producing a frame means issuing GPU draw calls with those parameters. Changing the matrix or the opacity changes a handful of numbers; the texture is untouched. There is no display-list re-recording, no rasterization, and — importantly — no main-thread work at all once a composited animation is handed over. ```css /* layout -> paint -> raster -> composite, every frame */ @keyframes slide-expensive { to { left: 300px; } } /* composite only, every frame */ @keyframes slide-cheap { to { transform: translateX(300px); } } ``` Both animations look identical. The first invalidates geometry sixty times a second; the second changes a matrix sixty times a second. ## The caveats that separate a good answer from a slogan **"Composite only" requires a compositing layer.** If the element is not promoted, changing its transform invalidates the paint recording of whatever layer it paints into, and the browser repaints and re-rasterizes that region. Browsers promote automatically when they see a running transform or opacity animation, which is why the rule holds for animations in practice — but a one-off transform write on an ordinary element is a repaint, not a magic trick. **Opacity is not unconditionally free either.** On a promoted layer, opacity is a compositor parameter. On a non-promoted element, changing opacity invalidates paint like any other visual property, and an opacity below 1 also forces the element's content to be composited as a group rather than blended element by element. **Scaling can still cost raster.** Translating a texture is resampling-free in practice, but animating `scale` upward can leave the layer rasterized at too low a resolution — the browser may re-raster at the higher scale to keep it sharp, which is real paint work. Some engines rasterize once at a chosen scale and accept blurriness instead; either way, scale is less uniformly cheap than translate. **The content still has to be painted once.** Promotion does not remove paint cost, it moves it: the layer is recorded and rasterized when it is created, and again whenever its contents change. An animation of `transform` on an element whose text is also being rewritten every frame repaints every frame. **Transform is not layout-neutral for everything.** A transformed element still occupies its original space in flow, but it does become a containing block for fixed and absolutely positioned descendants and it affects hit-testing and overflow. Those are semantics, not cost, but they are why swapping `left` for `transform` is not always a drop-in refactor. ## The mental model to state in an interview Name the stage each property enters and derive the cost from it. `left`, `width`, `margin` enter at layout — the most expensive entry point, because layout of one box can invalidate its subtree and its in-flow siblings. `background-color`, `box-shadow`, `border-radius`, `color` enter at paint — layout is skipped but the recording and its tiles die. `transform` and `opacity` enter at composite, on a promoted layer — nothing is recomputed, only the parameters used to draw an existing texture.

  • Is a transform write on any element composite-only, or does it depend on something?
    It depends on promotion. On an element with its own compositing layer the change is a matrix update. On a non-promoted element the browser must repaint and re-rasterize the region it paints into. Browsers usually promote automatically when a transform or opacity animation is running, which is why the rule holds for animations specifically.
  • Animating `transform: scale()` is sometimes less cheap than animating `translate()`. Why?
    A layer is rasterized at a particular scale. Translating just moves that texture, but scaling up means the texture no longer has enough resolution, so the browser may re-rasterize at the new scale to keep edges and text sharp — real paint work — or keep the old raster and accept a blurry frame.
  • Does an element with a transform still take up its original space in the layout?
    Yes. `transform` is applied after layout, so the box keeps its laid-out position and size, in-flow siblings do not shift, and text does not reflow around it. It does become a containing block for absolutely and fixed-positioned descendants, and it changes hit-testing and overflow.
  • Where does changing `background-color` sit relative to these two?
    In between. It skips layout — geometry is unaffected — but it invalidates the layer's paint recording and its rasterized tiles, so the browser re-records and re-rasterizes that region before compositing. Cheaper than a layout change, materially more expensive than a matrix update.

saying these in an interview costs you the question

  • Claims transform is always free regardless of promotion
  • Says left is slow only because it is an older property
  • Thinks transform changes the element's position in flow
  • Believes GPU-accelerated means no main-thread cost ever exists
  • Assumes opacity below 1 never causes any repaint

context

open as a page

In a browser's rendering pipeline, what does the paint step actually produce, and how is that different from what the compositing step does?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Paint records the drawing commands for each box — backgrounds, borders, text, shadows — into a list, per layer. Compositing rasterizes those layers into GPU textures and assembles them, with each layer's transform and opacity applied, into the frame on screen.

open as a page

What makes a browser give an element its own compositing layer, and why can promoting one element cause neighbouring elements to get layers too?

level: middleimportance: should knowfreq 45%

basics

~20 s

Browsers promote content that must be drawn independently: 3D transforms, running transform or opacity animations, a will-change hint, video and WebGL canvas, and some fixed or scrolling content. Content that paints above a promoted layer may also need its own layer so the original paint order survives.

open as a page

A browser tab's main thread is blocked by a long synchronous JavaScript task. A CSS `transform` animation keeps moving smoothly while a JavaScript-driven animation of the same element freezes. Why?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The CSS animation was handed to the compositor thread, which owns the layer's texture and can tick the transform and draw frames without the main thread. The JavaScript animation writes styles from main-thread callbacks, so while that thread is blocked nothing updates.

open as a page

What does declaring `will-change: transform` in CSS ask the browser to do, and what goes wrong when it is applied to many elements or left declared permanently?

level: seniorimportance: should knowfreq 50%

basics

~20 s

will-change is a hint that a property is about to change, letting the browser prepare — usually by creating a compositing layer up front. Applied broadly or left on permanently it keeps those layers alive, consuming GPU memory and compositor bookkeeping, which slows the page instead of speeding it up.

open as a page