Transform versus layout: measure the actual rendering work

Comparison Last tested 24 October 2026

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, msp95 interval, msLoAF entriesLayout eventsPaint events
116.717.6092184
216.617.6092184
316.717.6092184
416.717.7092184
516.717.6092184

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

Sources

  1. web.dev, Rendering performance, living article. https://web.dev/articles/rendering-performance (opened 24 October 2026). ↩

  2. MDN, PerformanceLongAnimationFrameTiming. Living documentation, opened 24 October 2026. ↩

  3. MDN, transform, living documentation. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/transform (opened 24 October 2026). ↩

  4. 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.