Transform versus layout: measure the actual rendering work
Choose a property that produces the required geometry, then measure the rendering work on the actual page before claiming a performance improvement. Translation can move a visual box without asking its neighbors to make room; changing width solves a different problem when surrounding layout must respond.
Make the visual task comparable
The first two lanes move a 64-pixel parcel 120 CSS pixels to the right. One uses transform: translateX() and the other changes a relatively positioned element’s left value. Both retain a following label called Next. The third lane grows the parcel from 64 to 184 pixels, allowing its neighbor to move in the flex layout.
The width lane is intentionally not a visual equivalent of the two translation lanes. It answers a different requirement: reserve additional space. Treating all three as interchangeable would hide a content decision inside a performance comparison. First decide whether the surrounding content should move; then choose which implementations belong in the same comparison.
All three use the same authored duration of 1,500 milliseconds and linear easing. This controls two inputs to the example. It does not make their rendering work identical, and it does not prove that any run met a display refresh deadline. The component itself records no timing data. The separate measurement wrapper preserves its code and adds an observer; its combined workload cannot attribute cost to an individual lane.
Keep the rendering stages distinct
The browser rendering pipeline separates style calculation, layout, paint and composition. A change to geometry can require layout and subsequent rendering work; which stages occur is the question to investigate in a trace 1. The same screen position is therefore not evidence of the same work, just as a different property name is not evidence of a measured speed difference.
For a comparison intended to support a performance claim, hold the content, viewport, animation duration, event sequence and measurement window constant. Capture each implementation separately after confirming that it fulfills the same visual requirement. Keep warm-up and repeated trials explicit. A screenshot can check the endpoint, but it cannot count layout events or establish a frame-duration distribution.
Keep the measurement conditions beside the result. The OS display-mode value is independent of callback spacing, but does not prove the timing of physical presentations.
How we measured it
Under the Tested conditions, the collector reset the component, marked the range, activated Run changes programmatically and sampled successive requestAnimationFrame timestamps. We retained intervals whose two timestamps fell inside the nominal window. A supported, active PerformanceObserver collected long-animation-frame entries overlapping that window. The API identifies rendering updates delayed beyond 50 ms.2 Zero received entries is not a missed-presentation count.
Each median uses the sorted middle value; p95 uses nearest rank, ceil(0.95 × n). Every measured run contained 119 intervals. Values below are rounded to one decimal; full precision remains in the raw suite and recomputed analysis.
| Run (2 s window) | Median interval, ms | p95 interval, ms | LoAF entries | Layout events | Paint events |
|---|---|---|---|---|---|
| 1 | 16.7 | 17.6 | 0 | 92 | 184 |
| 2 | 16.6 | 17.6 | 0 | 92 | 184 |
| 3 | 16.7 | 17.6 | 0 | 92 | 184 |
| 4 | 16.7 | 17.7 | 0 | 92 | 184 |
| 5 | 16.7 | 17.6 | 0 | 92 | 184 |
The trace extraction counts literal Layout and Paint events between the UserTiming marks in a filtered Performance-panel export retaining the target renderer thread. Nested Paint events can overlap: neither their count nor summed inclusive duration is a frame count. The collector’s stop timer was up to 0.5 ms early; no samples were invented to fill the boundary. Profiling overhead and the combined workload prevent a transform-versus-layout speed conclusion.
Inspect neighboring geometry
In the component, each parcel is a fixed flex item and each lane has the same gap. Compare the parcel’s rectangle, its layout width and the following label’s rectangle before and after Run changes. That separates the visible position from the width reserved for the item in the lane.
The transform and relative-left cases deliberately retain their original width. Their following labels provide a reference for the surrounding flow. In the width case, the parcel’s declared width changes, so the next label is part of the geometric result being examined. None of these readings describes paint cost or layer allocation.
Use the overlap itself as useful information. A translated parcel can occupy space that was not reserved for it, including the space where its neighbor is drawn. If that is inappropriate for the intended interface, choosing a property with a different rendering path does not solve the underlying requirement. Change the layout or interaction design rather than accepting the wrong geometry because it looks promising in a microbenchmark.
Treat stacking and containment as part of the change
A transform other than none creates a stacking context and establishes a containing block for positioned descendants 3. That makes adding a transform a structural decision as well as a visual one. Review nested positioned content and overlapping siblings before applying the technique to a larger component.
This original comparison has no nested fixed-position tooltip, sticky descendant or transformed ancestor. Its simple lanes avoid those concerns so that the geometric difference remains inspectable. Adding such content would create new questions; the current run would not answer them automatically.
The source assigns a final inline value after each animation completes, then cancels its filling effect. Reset cancels outstanding effects and restores all three starting values. A generation check prevents an obsolete completion handler from writing endpoints after Reset. These are state-management choices shared across the cases, keeping unrelated lifecycle differences out of the geometric comparison.
Add a hint only for an observed problem
will-change is a hint that can affect resource use. MDN advises using it sparingly for existing performance problems and warns that excessive use can increase memory and rendering complexity 4. The component therefore includes no automatic layer hint or forced three-dimensional transform.
If a measured problem justifies trying a hint, compare before and after with the same workload and include the resource cost you can actually observe. A useful trial might remove the hint after the interaction and inspect whether the retained visual state stays correct. Do not infer a benefit from seeing a layer or from the hint’s presence in computed styles.
The component
Original code, MIT licensed. Save this page locally. The separate demo includes the geometry-check sequence; it supplies no benchmark score.
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Compare translation and size changes</title>
<style>
body { font: 1rem/1.5 system-ui; margin: 1rem; }
.lane { display: flex; gap: 8px; width: 360px; max-width: 100%; margin: 1rem 0; }
.sample { position: relative; flex: 0 0 auto; width: 64px; padding: .5rem 0; background: #174f76; color: white; text-align: center; }
.neighbor { align-self: flex-end; border: 1px solid; padding: .25rem; }
button { padding: .5rem; margin: .25rem; }
:focus-visible { outline: 3px solid #b24d00; outline-offset: 3px; }
</style>
</head>
<body>
<button id="move">Run changes</button>
<button id="reset">Reset</button>
<p>Transform translation</p>
<div class="lane">
<div class="sample" id="transform">Parcel</div>
<span class="neighbor">Next</span>
</div>
<p>Relative left offset</p>
<div class="lane">
<div class="sample" id="layout">Parcel</div>
<span class="neighbor">Next</span>
</div>
<p>Width change: a different layout requirement</p>
<div class="lane">
<div class="sample" id="width">Parcel</div>
<span class="neighbor">Next</span>
</div>
<p id="state" role="status">Ready</p>
<script>
const state = document.querySelector("#state");
const cases = [
{ element: document.querySelector("#transform"), property: "transform", from: "translateX(0px)", to: "translateX(120px)" },
{ element: document.querySelector("#layout"), property: "left", from: "0px", to: "120px" },
{ element: document.querySelector("#width"), property: "width", from: "64px", to: "184px" }
];
let running = [];
let generation = 0;
function reset() {
generation += 1;
running.forEach(animation => animation.cancel());
running = [];
cases.forEach(item => item.element.style[item.property] = item.from);
state.textContent = "Ready";
}
document.querySelector("#move").onclick = () => {
reset();
const ticket = generation;
const duration = matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 1500;
state.textContent = "Changing";
running = cases.map(item => item.element.animate(
[{ [item.property]: item.from }, { [item.property]: item.to }],
{ duration, easing: "linear", fill: "forwards" }
));
Promise.all(running.map(animation => animation.finished)).then(() => {
if (generation !== ticket) return;
cases.forEach(item => item.element.style[item.property] = item.to);
running.forEach(animation => animation.cancel());
running = [];
state.textContent = "At endpoints";
}).catch(error => {
if (error.name !== "AbortError") state.textContent = "Animation error";
});
};
document.querySelector("#reset").onclick = reset;
</script>
</body>
</html>
Read the result at the right level
Run changes establishes endpoints for inspection. Reset returns to the baseline; resetting during a run tests whether the abandoned completion can overwrite that baseline later. Repeat those state checks before collecting performance measurements, because a mismatched endpoint makes a timing comparison difficult to interpret.
Use this combined result to decide what must be held equal in an isolated follow-up. The frame-timing guide explains timestamp-based progress, and the metrics reference separates observable timing quantities. Neither a progress value nor a geometry match is a substitute for a rendering trace.
Not covered on this page
- An isolated technique ranking or displayed-frame duration distribution.
- Layer memory, compositor-thread attribution or battery use.
- Nested transformed containing blocks or a universal responsive layout.
- Real touch, assistive-technology testing or accessibility certification.
Sources
-
web.dev, Rendering performance, living article. https://web.dev/articles/rendering-performance (opened 24 October 2026). ↩
-
MDN, PerformanceLongAnimationFrameTiming. Living documentation, opened 24 October 2026. ↩
-
MDN, transform, living documentation. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/transform (opened 24 October 2026). ↩
-
MDN, will-change, living documentation. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/will-change (opened 24 October 2026). ↩
Code on this page is original and MIT licensed.
How we test: methods. Found a mistake? Corrections.