ENGINE-SPECIFIC

What Does This Change Cost?

Pick a change and see which stages of the rendering pipeline have to run again. This is the question behind most frontend performance advice, and knowing the answer turns a list of rules into something you can derive.

“Maybe” is a real answer and it appears a lot. Whether a change costs layout or only compositing depends on what else is on the page — whether the element is on its own layer, whether containment is applied, and what else changed in the same frame. A table that answered every cell yes or no would be teaching a certainty the pipeline does not have. Engines also differ here, and the same change can be free in one and not in another.

204 of 204 changes
Adding `width` and `height` attributes
Styleno

Recompute which declarations win for the affected elements and what their computed values are.

Layoutno

Compute geometry — size and position — for everything the change can affect, which is often more than the element you changed.

Paintno

Record the drawing commands for the affected area and rasterise them.

Compositeno

Assemble the layers into the frame that is handed to the screen.

Why, as a mechanism

Pure gain: the ratio is known before the bytes arrive, so the first layout is already correct and the second one never happens.

caveat Every maybe here resolves against the rest of the page. An image inside a container with a fixed size and contain: layout cannot propagate a layout invalidation outwards at all, which changes several of these rows (CSS Containment).

The whole table

Every change the lessons cost out, in one place. Read down a column to see which stage is the expensive one for you.

ChangestylelayoutpaintcompositeWhy
Adding `width` and `height` attributesnonononoPure gain: the ratio is known before the bytes arrive, so the first layout is already correct and the second one never happens.
An image arriving with no declared rationoyesyesmaybeThe box grows from zero to its intrinsic size, invalidating layout for everything after it in the flow. This is the mechanism of media-driven layout shift.
Swapping `src` on a visible imagenomaybeyesmaybeLayout only if the intrinsic ratio changed or no ratio was declared. Paint always; a new decode and a new texture upload.
`loading="lazy"` on an offscreen imagenonononoDefers the request, the decode and the texture entirely — as long as space was reserved, so its later arrival shifts nothing.
A playing `<video>`nononoyesFrames go straight to a composited surface, often from hardware decode. Cheap per frame on the main thread and continuous for the whole duration.
Inlining an SVG instead of using `<img>`yesyesyesnoThe graphic becomes DOM: every node is styled and laid out. Sixty inline icons is sixty subtrees through every style pass (Style Calculation).
`object-fit` change on a loaded imageyesnoyesmaybeThe box is unchanged; only how the resource is fitted into it changes, so the geometry pass is skipped.
Append a paragraph inside a normal block flowyesyesyesyesStyle is resolved for the new nodes; layout runs for the containing block and anything after it in flow. Local and cheap.
Append a row to a table with auto layoutyesyesyesyesColumn widths depend on all cells, so a new row can relayout every row already on screen. table-layout: fixed makes this local instead (Layout Thrashing).
Append content into a reserved-size skeletonyesmaybeyesyesIf the reserved box already has the final dimensions, layout is confined inside it and nothing after it moves — this is the whole point of reserving space.
Append an image with width and height attributesyesnomaybeyesThe box is sized from the aspect ratio before any bytes arrive, so decode and paint happen later without a second layout (Responsive Images).
Append an image without dimensionsyesyesyesyesZero-height until decoded, then it pushes everything below it down. The classic source of visual instability (Visual Stability).
Append a stylesheet link mid-documentyesmaybeyesmaybeRendering is blocked until it parses, then every element it matches is restyled — potentially the entire document already on screen (Render-Blocking Resources).
Append inside a subtree with `content-visibility: auto`maybemaybenonoOffscreen subtrees can skip layout and paint until they approach the viewport, which bounds the per-chunk cost of a long document (content-visibility).
`el.textContent = "Saved"`noyesyesyesNo selector could match differently, so style is untouched — but text metrics changed, so the line box, the containing block and anything sized by content must be measured again.
`el.classList.add("is-active")` setting only `color`yesnoyesyesThe browser must recompute style to find out what changed. Having compared old and new computed values, it can see no geometry input moved and skips layout.
`el.classList.add("is-open")` setting `height`yesyesyesyesIdentical code to the row above, entirely different cost. The class name tells you nothing; the declarations inside it tell you everything.
`el.style.transform = "translateX(8px)"`yesnomaybeyesInline style writes always recompute this element's style. Paint is skipped only if the element already has its own compositing layer; otherwise its layer's contents are re-rastered.
`el.style.opacity = "0.5"`yesnomaybeyesSame shape as transform. Opacity below 1 usually creates a stacking context, which can change how much is grouped into one layer (Positioning and Stacking Contexts).
`el.style.top = y + "px"` in a rAF loopyesyesyesyesThe visual result can be identical to a transform animation and the cost is the entire pipeline, every frame. This is the single most common cause of a janky animation.
`container.appendChild(node)`yesyesyesyesA new box in the flow. Siblings after it may move; the containing block may resize; :nth-child, :last-child and sibling combinators can invalidate neighbours you did not touch.
`container.innerHTML = sameMarkup`yesyesyesyesThe full cost for zero visual change, plus destroyed listeners, lost focus, lost selection, reset scroll and restarted transitions. The browser cannot detect that the output is identical (Node Identity Across Updates).
`document.body.classList.add("dark")`yesmaybeyesyesInvalidation scope is decided by which selectors descend from the changed element, not by the element itself. A theme class on the root is a document-wide style recalculation by design.
Reading `el.offsetHeight` after a writeyesyesnonoThe read changes nothing. It forces the browser to run style and layout *now*, inside your task, instead of at the rendering opportunity — and it does so on every iteration of a loop (Layout Thrashing).
`el.remove()` inside a `contain: strict` subtreeyesmaybemaybeyesContainment tells the browser that nothing inside can affect the size or paint of anything outside, so invalidation stops at the boundary instead of propagating to the document (CSS Containment).
`el.setAttribute("aria-expanded", "true")`maybenononoCosts nothing visually unless a selector matches on the attribute — but it does invalidate the accessibility tree, which is the entire point of writing it (The Rules of ARIA).
Toggle a class on one elementyesmaybemaybeyesOne invalidation. Layout only if the winning declarations change geometry; the engine knows which properties those are and skips layout when none did.
Toggle a class on `<html>` (theme switch)yesmaybeyesyesThe invalidation set is the whole document. Cheap in code, the single most expensive style operation most applications perform (Style Invalidation).
Write `el.style.transform`yesnonoyesStyle recalculation for one element, then the compositor handles it. This is the mechanism behind the advice, not a magic property (Cheap and Expensive Animation).
Write `el.style.width`yesyesyesyesGeometry changed, so the browser must lay out this box and anything whose size or position depends on it.
Insert a `<style>` elementyesmaybemaybeyesParse plus re-index plus a document-wide invalidation, because the new rules could match anything.
Set `sheet.disabled = true`yesmaybemaybeyesNo re-parsing — the index already exists — but every element that matched a rule in it must be recomputed.
Call `getComputedStyle(el).width`yesyesnonoNot a change at all: a *read* that forces pending style and layout to be flushed so the browser can give you a real number (Layout Thrashing).
`getComputedStyle(el).color`yesnononoFlushes pending style only. color is fully resolved at computed-value time, so no geometry is needed.
`getComputedStyle(el).width`yesyesnonoA used value. The browser must finish layout before it can answer with a number.
`el.getBoundingClientRect()`yesyesnonoAlways a used-value read. Cheap once per frame, ruinous once per list item (Layout Thrashing).
Change `color` on `:root`yesnoyesyesInherited, so every descendant is invalidated — but no geometry changed, so layout is skipped.
Change `font-size` on `:root`yesyesyesyesInherited *and* geometric. Every descendant restyles and the whole document relayouts. The most expensive one-line change in CSS.
Toggle `display: none` on a subtreeyesyesyesyesBoxes are destroyed and everything after it reflows. Also removes the subtree from the accessibility tree.
Toggle `visibility: hidden` on a subtreeyesnoyesyesInherited, and the box is preserved, so surrounding geometry does not move. Still removed from the accessibility tree.
Set a colour token on `:root` (theme toggle)yesnoyesyesEvery element inherits it, so the invalidation set is the document — but nothing geometric changed, so layout is skipped. Once per toggle is fine.
Set a spacing token on `:root`yesyesyesyesInherited *and* consumed by geometric properties. The whole document restyles and relayouts (Style Invalidation).
Set a token on one component rootyesmaybemaybeyesInvalidation is bounded by the subtree. Layout only if the token feeds a geometric property inside it.
Set a token on `:root` from `pointermove`yesmaybeyesyesThe same document-wide work, per input event. This is the common way custom properties become a performance bug (The Frame Budget).
Transition a registered `<color>` propertyyesnoyesyesInterpolated per frame, and each frame restyles whatever reads it. Bounded if the property is scoped and inherits: false.
Transition a registered `<length>` used for `width`yesyesyesyesLayout every frame for the duration. Animate a compositable property instead where the visual result allows (Cheap and Expensive Animation).
`getComputedStyle(el).getPropertyValue('--x')`yesnononoA read that flushes pending style. Custom properties are resolved at computed-value time, so no layout is required — but it is still a synchronisation point.
Toggle a class used only as a key selector, no geometry in the ruleyesnoyesyesOne element recalculated; the rule changes colour only, so layout is skipped entirely.
Toggle a class on an ancestor that many descendant rules mentionyesmaybemaybeyesThe recalculation covers the subtree. Whether layout follows depends on whether any winning declaration is geometric — often it is not, and the cost is pure style.
Set a custom property on `:root` consumed by hundreds of elementsyesmaybemaybeyesSubstitution runs per consumer. If the variable feeds a length used in sizing, layout follows for all of them.
Insert one row into a list styled with `:nth-child`yesyesyesyesSibling invalidation for everything after it, plus real layout for the new box — the two costs compound as the list grows.
Change `width` or `height` on an in-flow elementyesyesyesyesGeometry changed. Siblings may move, and ancestors sized by their content may resize, unless a containment boundary stops the propagation (CSS Containment).
Change `margin` or `padding`yesyesyesyesSame as size: these participate in the box model, so the boxes around them move too (The Box Model).
Change `top` / `left` on a positioned elementyesyesyesyesStill layout, unlike transform. On an absolutely positioned element the dirty region is smaller because it is out of flow, but layout runs for it regardless.
Change `font-size`yesyesyesyesInherited, so the invalidated set includes descendants, and text metrics change so every line box is recomputed.
Change `color`yesnoyesyesInherited — the style stage touches descendants — but nothing moves, so layout is skipped entirely.
Change `background-color`yesnoyesyesPure paint. Cost tracks the painted area, not the number of elements.
Change `box-shadow` (large blur radius)yesnoyesyesNo geometry, but blur is one of the more expensive things to rasterise, and the cost scales with radius and area every frame it changes.
Set `visibility: hidden`yesnoyesyesThe box keeps its space, so geometry is unchanged. The subtree does leave the accessibility tree.
Toggle `display: none` ↔ `block`yesyesyesyesBoxes are destroyed and recreated; on re-show, style and layout run for the whole subtree as if it were new.
Animate `transform` on an element with its own composited layeryesnonoyesThe compositor applies a new matrix to already-rasterised content. This is the path everyone means by "cheap animation" — and it requires the layer to exist.
Animate `transform` on an element with no layer of its ownyesnoyesyesNo layout — transforms never reflow — but the content must be redrawn in its new position within the layer it shares.
Animate `opacity` on a composited elementyesnonoyesAn alpha value applied at composite time. Same precondition as transform.
Animate `opacity` on a non-composited elementyesnoyesyesThe subtree is repainted at the new alpha every frame; on a large overlay of text this is the whole frame budget.
Animate `filter: blur()`yesnomaybeyesSome filter functions can be applied by the compositor and some cannot, per engine. Either way a large blur is expensive per frame — the question is only which processor pays.
Add `will-change: transform`yesnomaybeyesUsually triggers promotion: a new layer, GPU memory, and an initial raster of the contents. Beneficial before an animation, a standing cost if left on.
Append a DOM nodeyesyesyesyesA new box needs style and geometry, and sibling selectors may invalidate the elements after it (What a Mutation Costs).
Remove a DOM nodeyesyesyesyesThe space it occupied has to be redistributed, and following siblings may match different rules than before.
Change the text inside an elementmaybeyesyesyesStyle may not need recomputation at all, but line breaking and box sizing do — which is why a live-updating counter can be surprisingly expensive in a flex row.
Toggle a class matched only as a key selectoryesmaybemaybeyesOne element recalculated; what follows depends entirely on which declarations that rule contains.
Toggle a class on `<body>` used by descendant selectorsyesmaybemaybeyesThe style cost is the invalidated set, which can be the document. Layout follows only if a winning declaration is geometric (Style Invalidation).
Set a CSS custom property on `:root`yesmaybemaybeyesSubstitution runs per inheriting consumer. If the value feeds a length, layout follows for all of them; if it feeds a colour, only paint does (Custom Properties).
Read `offsetHeight` after writing a styleyesyesnonoForced synchronous layout: style and layout run immediately, inside your task, and produce no pixels. In a loop, once per iteration.
Call `getBoundingClientRect()`yesyesnonoSame as above — the browser must flush pending invalidation to return a correct rectangle.
Scroll a container the compositor ownsmaybenomaybeyesScrolling moves already-rastered tiles. Style appears when :hover targets change or scroll-driven effects run; paint appears for newly exposed content (Scroll and Input Latency).
Scroll with a non-passive listener that reads layoutmaybeyesmaybeyesThe scroll now depends on the main thread: the compositor must wait for the handler, and the handler forces layout. This is the classic scroll-jank recipe (Passive Listeners).
Add a stylesheet at runtimeyesyesyesyesNew rules mean rebuilt indexes and no safe assumption about what matched before; treat it as a document-wide invalidation.
A web font finishes loadingyesyesyesyesText metrics change for every element using the family, which is why late fonts produce layout shift after content is already readable (Images and Fonts).
`el.offsetWidth` / `offsetHeight` / `offsetTop`yesyesnonoFlushes pending style and layout so the integer is correct. No frame is produced.
`el.getBoundingClientRect()`yesyesnonoSame flush, sub-pixel result. Also reflects transforms, which offsetTop does not.
`getComputedStyle(el).color`yesnononoStyle must be current; a non-geometric property does not require layout.
`getComputedStyle(el).height`yesyesnonoA resolved length is a used value, so layout has to run — the same call is cheap or expensive depending on the property you ask for.
Reading `el.scrollTop`yesyesnonoScroll offsets are geometry. Writing scrollTop does not force layout, but reading it does — which is why scroll handlers thrash so easily.
`ResizeObserver` / `IntersectionObserver` callback datanonononoThe measurement was taken by the engine during the rendering steps and handed to you. This is the point of these APIs: geometry without a forced flush.
`width`, `padding`, `border-width`, `margin`yesyesyesyesGeometry changes, so every box whose position depends on this one must be recomputed, then repainted in its new place.
`box-sizing`yesyesyesyesIt changes what width resolves to, so it is a geometry change wearing a different name.
`border-color`yesnoyesyesSame box, different pixels. Nothing moves, so layout is skipped entirely.
`outline`, `box-shadow`yesnoyesyesPainted outside the border box and excluded from geometry by design — this is exactly why focus rings do not reflow the page.
`transform: scale()`yesnomaybeyesThe layout box is untouched; the compositor transforms already-painted content. Repaint only if the element must be re-rasterised at the new scale for sharpness.
`aspect-ratio` on an image with no dimensionsyesyesyesyesIt costs layout once, at parse time, and saves the far worse layout that would have happened when the image decoded (Visual Stability).
`color` on a text nodeyesnoyesyesGeometry is unchanged, but the glyphs must be re-rasterised in the new colour over the damaged text region.
`background-color` on a cardyesnoyesyesOne fill command over the card's rectangle — about as cheap as a repaint gets, and the cost is the area.
`box-shadow` blur radiusyesnoyesyesA convolution over a region larger than the element. Cost scales with blur radius and device pixel ratio, not with element size alone.
`border-radius` on a scrolling containeryesnoyesmaybeA non-rectangular clip applied to everything inside; it can force the contents onto a separate surface so the clip can be applied at composite time.
`width` on a flex itemyesyesyesyesGeometry changes for the item and typically its siblings, so paint follows layout across the whole flex line (Layout Thrashing).
`transform: translate` on a promoted layeryesnonoyesThe bitmap already exists; the compositor draws it at a different offset. Only true while the element genuinely has its own layer (Cheap and Expensive Animation).
`opacity` on non-promoted contentyesnomaybeyesWithout a layer this is a blend performed during raster, and it forbids skipping whatever is underneath.
`visibility: hidden`yesnoyesyesThe box keeps its geometry, so no layout — but the region it occupied must be repainted with whatever is behind it.
`filter: blur()` on a hovered imageyesnoyesmaybeFilters often promote the element, which moves the cost from repaint-per-frame to memory-plus-one-raster — a trade, not a win.
`transform` — CSS animation, promoted elementnononoyesDelegated. The compositor interpolates and redraws the existing texture. The main thread is not involved after the initial commit.
`transform` — CSS animation, not promotedyesnomaybeyesNo layer to move, so the transform is applied where the content is drawn. No layout, but the main thread is in the loop every frame.
`transform` — written per frame from JavaScriptyesnomaybeyesThe interpolation is your callback on the main thread. Same property, entirely different scheduling.
`opacity` — CSS animation, promoted elementnononoyesDelegated: an alpha applied to an existing texture at blend time.
`opacity` — non-promoted contentyesnomaybeyesA blend at raster time, which also forbids skipping the content underneath. Cost scales with the area, not the element count.
`left` / `top` on a positioned elementyesyesyesyesPosition is geometry. Layout runs for the element and anything whose position depends on it, every frame.
`width` / `height`yesyesyesyesThe most expensive common animation: it can reflow siblings, ancestors and text line boxes on every frame.
`background-color`yesnoyesyesNo geometry change, but the fill is re-executed over the element's area every frame.
`filter: blur()`yesnomaybeyesOften promoted and sometimes delegated, but the convolution itself still runs per frame — delegation moves the thread, not the arithmetic.
No promotion; one card animates transformyesnomaybeyesThe engine promotes the animating card for the animation's duration and demotes it afterwards. One temporary layer, no permanent cost.
`will-change: transform` on the animating card only, scoped to the interactionyesnonoyesOne layer for a fraction of a second, and no blank first frame. This is the version the advice is actually about.
`will-change: transform` in the card's base class, 200 instancesyesnonoyesTwo hundred permanent bitmaps. The animation is delegated and everything else — scroll, memory, compositing per frame — is worse.
`will-change: transform` on the scroll container insteadyesnomaybeyesOne enormous layer covering the whole list, tiled. Cheaper than 200 layers, still a large allocation, and it does not help the per-card animation at all.
Containment on each card, no promotionyesnoyesyesInvalidation is isolated without allocating a surface. Paint still runs, but only for the card that changed (CSS Containment).
`content-visibility: auto` on off-screen sectionsmaybenonoyesOff-screen content is skipped entirely — no style, no layout, no paint until it approaches the viewport (content-visibility).
Typing in an uncontrolled inputyesmaybeyesyesThe browser updates the value and repaints the text. Layout only if the field grows — an auto-sizing textarea, for instance. No application code runs.
Controlled input, state scoped to the fieldyesmaybeyesyesSame browser work plus a small render. The added cost is JavaScript time inside the interaction, not extra pipeline stages.
Controlled input, state at the top of a large formyesmaybeyesyesStill the same pipeline stages, but the render diffs the entire form per character. The symptom is a delayed frame, not extra layout.
Controlled state driving a filtered listyesyesyesyesNow the DOM genuinely changes: rows are added and removed, so layout and paint are unavoidable. This is where debouncing the derived work pays (List Virtualization).
Validity class toggling on `:user-invalid`yesmaybeyesnoA colour or border change repaints; layout only if the error message changes the element's box.
Error message appearing below the fieldyesyesyesyesInserting a node reflows what follows it. Reserving the space up front avoids the content shift (Visual Stability).
Adding `container-type: inline-size`yesyesmaybenoIt applies layout, style and size containment in the inline axis. The element stops sizing to its content inline and establishes an independent formatting context — a genuine geometry change, applied the moment the declaration lands.
Adding `container-type: size`yesyesmaybenoContainment in both axes, so the element no longer sizes to content at all. An automatic height collapses to zero. This is the "my component vanished" case.
A container resizing across a `@container` boundaryyesyesyesmaybeThe query re-evaluates during layout, the subtree restyles, and it is laid out again. Containment is what guarantees the second pass cannot change the container and start a third.
The `ResizeObserver` equivalentyesyesyesmaybeSame rendering work, plus a main-thread callback per observed element, plus a forced layout read, plus a one-frame lag in which the wrong variant is on screen (Layout Thrashing).
Reading a `cqi` unit instead of `vw`yesmaybemaybenoResolving against the container rather than the viewport is the same arithmetic against a different reference. It becomes layout-relevant only because the container size is itself a layout output.
Re-render producing identical outputnonononoReconciliation finds no difference and emits no mutation. The cost is framework work only, and it is usually microseconds.
Text content changes, same box sizeyesmaybeyesyesStyle resolves for the affected node and paint redraws it. Layout is skipped only if the new text does not change the box — with intrinsic sizing anywhere above it, it will (Intrinsic Sizing and the Automatic Minimum).
Class toggled that changes colour onlyyesnoyesyesA paint-only property. Style must recompute for the subtree the selector affects, which can be far wider than the element you had in mind.
Class toggled that changes widthyesyesyesyesGeometry changed, so layout runs for the containing block and everything it affects. The full pipeline, every time.
Row inserted into a listyesyesyesyesNew nodes need style and boxes, and siblings after it move. Cost scales with what follows the insertion point, not with the row itself.
List reordered with stable keysmaybeyesmaybeyesThe framework moves existing nodes rather than recreating them. Positions change so layout runs; paint may be reusable if nothing about the nodes changed.
Same reorder with index keysyesyesyesyesThe framework matches by position, so it rewrites the contents of every row instead of moving any. Focus, scroll and per-row state are destroyed as well (Node Identity Across Updates).
Animating `transform` on a composited elementnononoyesHandled by the compositor without re-running style, layout or paint — provided the element already has its own layer and nothing else forces it back (Compositing Layers).
Reading `offsetHeight` after a writeyesyesmaybemaybeForces the browser to compute layout synchronously before the read returns. In a loop this runs once per iteration and is the classic frontend performance bug (Layout Thrashing).
Root boundary: whole page becomes a centred spinneryesyesyesyesEvery element is unmounted and re-created. Style is recomputed for the entire document, layout runs over all of it, and the whole viewport repaints — twice per navigation, since the content comes back the same way.
Outlet boundary: one pane becomes a skeleton of the same sizeyesmaybeyesmaybeStyle for the new nodes only. Layout is avoidable if the skeleton reserves the same box and the container does not size to its content; paint is limited to the pane's area.
Retain the old view, add an inline determinate progress baryesnoyesmaybeNothing unmounts, so no geometry changes. If the bar is animated with transform on its own layer, the per-frame update is compositor work rather than paint (Cheap and Expensive Animation).
Fade the retained view to reduced opacity while pendingyesnonoyesOpacity on an already-promoted layer is a compositor-only change: no geometry, no repaint of the contents. The cheapest honest pending signal available.
Skeleton replaced by content of a different heightyesyesyesyesThe shift the user actually feels. Everything after the boundary in flow moves, which is a layout pass over the rest of the document and the reason skeleton dimensions matter (Visual Stability).
Append 20 rows to the end of a listyesyesyesyesNew boxes must be styled, positioned and painted. Existing rows above are usually untouched, which is what makes appending the cheap direction.
Insert 3 rows at the top of a long listyesyesyesyesEverything below moves, so layout runs over the whole list and the scroll position shifts under the reader — the reason to put new items behind a "3 new items" control (Visual Stability).
Replace the whole list (new filter)yesyesyesyesThe full cost of the list, plus discarding the old nodes. Keyed reconciliation cannot help when nothing is reused (Reconciliation and Keys).
Append with index-based keysyesyesyesyesEvery row is now associated with different data, so the framework updates all of them instead of adding twenty. The change is small and the work is proportional to the whole list.
Toggle a row's selected classyesmaybeyesmaybeLayout only if the rule changes geometry — a border or padding does, a background colour does not. Choose the property with that in mind (The Cost of a Change).
Scroll a virtualised window by one pageyesmaybeyesyesRows are recycled rather than added, so node count is constant; layout depends on whether row heights are fixed or measured (List Virtualization).
Show a spinner in reserved space at the list endyesnoyesyesThe space was already allocated, so nothing above it moves — which is the entire reason to reserve it.
Toggle a boolean on one row (star, read, pinned)yesnoyesyesA class or attribute change on one element. Geometry is unchanged, so the confirmation frame is usually a no-op if the server agrees.
Optimistically insert a row with a temporary idyesyesyesyesThe list grows, so everything below reflows. Then the real id arrives and the row is re-keyed — a second insert and remove unless identity is preserved.
Reorder a list by dragyesyesmaybemaybeReordering DOM nodes relayouts the container. A transform-based reorder can stay on the compositor, but only if positions are not also being written (Cheap and Expensive Animation).
Optimistic edit to a text field the server may normaliseyesmaybeyesyesLayout depends on whether the normalised text is a different length — which you cannot know, which is exactly the problem.
Rollback of any of the aboveyesmaybeyesyesA third render. Cheap in browser terms and expensive in user terms: it is the frame where the interface contradicts itself.
Prediction that recomputes a derived total or countyesmaybeyesyesThe derived value renders elsewhere on the page, so one optimistic write can invalidate regions the user is not looking at (Derived State).
Patch one field on one row (text content)yesmaybeyesyesStyle recalculation is scoped to the element; layout is only needed if the new text changes the box's intrinsic size, which for a fixed-width numeric column it usually does not.
Replace the whole collection, keys preservedyesmaybemaybeyesWith stable keys the framework patches in place, so the cost approximates the sum of the rows that actually changed rather than the size of the list (Reconciliation and Keys).
Replace the whole collection, identity lostyesyesyesyesEvery node is destroyed and recreated, so every box must be laid out and painted again — and focus and text selection inside the list are lost with the nodes.
Reorder rows after a resyncyesyesmaybeyesGeometry changes for everything after the first moved row; paint may be avoidable if the rows themselves are unchanged and the engine can reuse their painted output.
Remove rows deleted during the gapyesyesyesyesEverything below shifts up, which is a layout the user perceives as content jumping — worth animating or batching so it happens once (Visual Stability).
Apply 200 replayed events one at a timeyesyesyesyesThe stages are not the problem; running them up to 200 times is. Buffering into one commit per frame collapses this to roughly the cost of the final state (Yielding and Scheduling).
Update a status label outside the listyesnoyesyesReserve the label's space so that "Reconnecting" and "Live" occupy the same box; otherwise an honest status indicator becomes a source of layout shift.
Toggle `aria-disabled` and a description on an existing buttonyesnomaybenoAn attribute change re-runs style for that element; layout is untouched because geometry does not change. Paint only if the disabled state alters colours.
Toggle `hidden` / `display: none` on a controlyesyesyesyesThe box leaves or enters flow, so siblings move. This is the version users feel as a jump when capabilities arrive late (Visual Stability).
Replace a button element with a static text labelyesyesyesyesNew nodes, new intrinsic sizes, and focus is lost if the removed node held it (Node Identity Across Updates).
Toggle `visibility: hidden` on a controlyesnoyesmaybeThe box keeps its space, so nothing shifts — but it stays in the layout tree and, critically, is removed from the accessibility tree too, so it is not a way to keep it announced.
Render a whole permission-dependent region after a separate capabilities fetchyesyesyesyesA full insertion into flow, late, after the user has begun reading. Reserve the space or attach capabilities to the original response instead.
Image loads with no reserved dimensionsyesyesyesyesThe box goes from zero height to its intrinsic height, so every subsequent box in flow is repositioned and repainted.
Image loads with `width`/`height` or `aspect-ratio`nonoyesyesThe box was already the right size; only its content is new. This is the fix, stated as a cost table.
Web font swaps in with different metricsyesyesyesyesLine box heights and line breaking depend on font metrics, so text reflows wherever the family is used.
Web font swaps in with matched metricsyesmaybeyesyesOverrides make the fallback occupy the same space; layout may still run, but geometry does not change, so nothing visibly moves.
Banner inserted at the top of the flowyesyesyesyesEverything below it moves down by the banner height. Scroll anchoring may compensate for the scroll position but not for a user mid-tap.
Same banner as a fixed overlayyesmaybeyesyesOut of flow, so nothing after it moves; it covers content instead, which is a different trade rather than no trade.
Accordion expands on clickyesyesyesmaybeA real geometry change that the stability metric excludes because it followed input. Users still experience the jump.
Animating `transform` on a late elementnononoyesComposited: nothing in flow is disturbed. That is why it is the tool for motion that must not move neighbours.
Hydration matches: listeners attached, no DOM changenonononoThe intended path. The DOM is untouched, so none of the rendering stages run again — the cost is entirely the main-thread walk that produced the match.
A text node corrected in placenomaybeyesyesThe text must be repainted. Layout runs again only if the new string changes the size of its box, which is why reserving width turns a maybe into a no.
An attribute corrected (`aria-expanded`, `data-state`)maybemaybemaybemaybeDepends entirely on whether any selector matches on that attribute. If none does, this is a pure accessibility-tree change with no visual cost at all — and no visual signal either, which is why this class hides so well.
A class corrected on a containeryesmaybeyesyesStyle must be recomputed for the element and anything inheriting from it. Whether layout follows depends on which properties the class changes (The Cost of a Change).
A subtree discarded and rebuiltyesyesyesyesEvery node is constructed again, styled again, laid out again and painted again — over a region the browser had already finished. This is the visible flash users report.
Fallback to a full client render of the rootyesyesyesyesThe entire document body is replaced. The server render is now pure overhead: it cost a per-request render, delayed the first byte, and its output was thrown away (Client-Side Rendering).
A design token value changes (a spacing or colour custom property)yesmaybeyesyesThe DOM is byte-identical, so every assertion still passes. Whether layout runs depends on whether the token feeds a geometric property; if it does, everything downstream of it moves (Custom Properties).
A stylesheet rule's specificity changes and a different rule winsyesmaybeyesyesStructure is unchanged and computed style is not what tests assert on. This is the classic regression that only pixels catch (Specificity).
Translated text is much longer than the sourcenoyesyesyesSame DOM shape, same roles, same test queries — and a button label that now wraps out of its container (Intrinsic Sizing and the Automatic Minimum).
A container gains `overflow: hidden`yesmaybeyesmaybeThe clipped content is still in the DOM, so a query finds it and a visibility assertion in a simulated document may still pass. The user cannot read it.
A focus ring is removed by a resetyesnoyesnoNothing about focus behaviour changed, so focus assertions pass. The indicator that told a keyboard user where they are has gone (Keyboard Operability).
A stacking context changes and a menu renders behind contentyesnoyesyesThe menu is present, named and clickable by the test runner. To a person it is underneath something (Positioning and Stacking Contexts).
A handler is removed so the button does nothingnonononoThe mirror image: nothing reaches the pixels at all, so the visual test passes happily while the feature is dead. This is why the level is a complement, never a replacement.
Animating `transform` on a promoted elementnononoyesThe compositor can move an already-rasterised layer without the main thread. If the recording shows layout here, the element is not actually on its own layer (Compositing Layers).
Animating `left` or `top`yesyesyesyesPosition participates in flow, so geometry has to be recomputed every frame and the result repainted (Normal Flow, Overflow and Margin Collapsing).
Changing `opacity` on an element that is not promotedyesnomaybeyesOpacity never affects geometry. Whether paint is needed depends on whether the engine could isolate the element into a layer.
Toggling a class that changes `background-color`yesnoyesyesColour is a paint-only property, but the repainted area may be much larger than the element if effects overlap it (Paint Commands).
Reading `offsetHeight` after a style writeyesyesmaybemaybeThis is the forced synchronous layout devtools flags. In a loop it produces one layout per iteration (Layout Thrashing).
Inserting rows into a long listyesyesyesyesCost scales with how much of the tree is invalidated, not with the number of rows inserted — containment can bound it (CSS Containment).
Changing a custom property used across the pageyesmaybemaybemaybeEverything downstream of the property must be recomputed; whether layout follows depends entirely on which properties consume it (Custom Properties).
Adding a large blurred shadow on hoveryesnoyesyesGeometry is unchanged, but rasterising the effect is expensive and the invalidated region extends beyond the element (Cheap and Expensive Animation).
Toggle `data-theme` on the root, colour tokens onlyyesnoyesyesCustom properties inherit, so computed style is invalidated for every element that inherits them; geometry is untouched, so the browser repaints without re-measuring (Style Invalidation).
Change a spacing or type-scale tokenyesyesyesyesThe token feeds a geometric property, so boxes actually change size and the layout pass has to run over everything affected (Layout Thrashing).
Change a component token on one elementyesmaybemaybemaybeScoped to that element's subtree — but whether it costs layout depends on which property the token feeds, and whether it costs paint depends on whether the element is visible at all.
Change a motion-duration tokenyesnonomaybeNothing repaints from the change itself; it alters the duration of animations that start afterwards, and only touches the compositor if one of those animations is compositor-driven (Cheap and Expensive Animation).
Swap a whole stylesheet instead of re-pointing tokensyesmaybeyesyesThe new sheet must be fetched and parsed before it applies, so there is a window showing the old theme — the flash that re-pointing custom properties avoids entirely (Render-Blocking Resources).
Subtree swapped for the other branchyesyesyesyesNew elements need computed styles, their geometry is unknown, and everything after them in normal flow may move. This is the most expensive and most visible form of a flip.
Flag toggles a class that changes only `color`yesnoyesnoA paint-only property: the box does not change, so geometry is untouched and only the affected paint area is redrawn (Cheap and Expensive Animation).
Flag toggles `display: none` to `block`yesyesyesmaybeThe element re-enters flow, so its own geometry and its siblings' positions are computed for the first time. Whether a new compositing layer is involved depends on what the revealed content contains.
Flag gates a lazily imported componentmaybemaybemaybemaybeNothing costs anything until the chunk arrives; then the full mount happens at whatever moment the network delivers it, which is later and less predictable than a flip of already-loaded code (Lazy Loading).
Flag read, but both branches render identical DOMnonononoThe framework may still re-render. A re-render that produces the same DOM costs script time and nothing downstream — which is why "it re-rendered" and "it was expensive" are different claims (What a Component Costs to Render).
Content hidden until flags resolve (anti-flicker)yesyesyesnoThe shift has not been removed, it has been moved to before the first paint — and the page is now blank for as long as the flag service takes. Reserving space is usually the better trade (Visual Stability).

caveat A change that skips layout on a page with one compositor layer can still force it on a page with a hundred. Confirm in the Performance panel rather than from the row.