Layout-triggering properties have the highest animation cost
Aliases: layout animation · compositor transform · will-change · layout thrashing
What it is
Every animation frame has to finish the shortest pipeline the engine will allow. Changing top, left, width, height, or margin makes the document recompute geometry, so paint and composite run again behind it. Changing transform or opacity can often update an existing layer on the compositor alone. Layout-triggering properties cost the most names which stage of that pipeline you poke: layout > paint > composite. It explains where per-frame work comes from, not how bad a missed vsync feels, and not whether to disable the whole motion.
Why it happens
A frame is roughly style → layout → paint → composite. Geometric properties write used boxes, so layout must rerun the dependency chain, dirty nodes re-record paint, and the compositor then quads the layers. transform and opacity on their own layer can skip layout and paint; the compositor only changes a matrix or alpha. will-change or a 3D transform promotes the element so that shortcut is stable; promote too many nodes and you pay VRAM and uploads instead—space traded for skipping layout.
The same visual travel is “reflow the page every frame” or “move one layer,” depending on the property. Animating left from 0 to 100 makes later siblings re-solve their positions every frame; translateX leaves the layout rect alone and only the layer moves. Width animation almost cannot stay on the compositor, because line boxes and subtree geometry are changing.
Where it stops holding
Some SVG presentational attributes, canvas drawing, and game scene graphs do not use the CSS document pipeline, so this property split does not apply. UI that is already live-layout (drag-resize, split panes) is supposed to change geometry; faking width with transform desyncs hit-testing and scroll height. Print and static export have no per-frame pipeline. A tiny one-shot transition (tens of milliseconds, small area) may layout without a user-visible miss—but that does not license layout animation on every row in a scrolling list.
Applying it
- Default translation and fade to
transformandopacity. When width or height must change, transform a wrapper, or write geometry once at gesture end—notwidthevery frame. - Use
will-changebriefly on elements that will composite; remove it when the animation ends, so every card on the page is not promoted. - In a performance trace, Layout on every animation frame means the property is wrong; Composite-only is the state this leaf wants.
- Verify by shipping the same motion as
leftand astranslateon a mid-range device: frame time and Layout count should be clearly worse for the layout version. The compositor version must still hit-test at the visual position (sync an invisible geometry update if needed).