Animation Performance: transform and opacity Instead of Layout Properties
AI generated
60fps
ms
Performance · Rendering Pipeline · Compositor · CSS
Animation Performance: transform and opacity Instead of Layout Properties
Why some animations stutter and others run buttery smooth

Every animation passes through the browser's rendering pipeline of layout, paint, and composite, and animating width, top, or margin forces the browser into expensive recalculations on every single frame. This article shows why transform and opacity run almost for free on the compositor thread, how to use will-change correctly, and how to refactor existing animations for real gains.

14 min. read Rendering Pipeline · Compositor · 60fps CSS Animations · will-change · requestAnimationFrame

1. Why animation performance makes the difference

A 60 Hz screen refreshes every 16.7 milliseconds. For an animation to be perceived as smooth, the browser must fully complete style calculation, layout, paint, and composite within that exact window before the next frame is due. If the time runs out, a frame gets dropped, visible as a brief stutter or jank. Unlike load times, where users tolerate some wait, torn animations stand out immediately because the human eye is extremely sensitive to irregularities in motion.

In Magento and Hyvä stores, this affects every interaction with visual feedback: opening the mobile menu, sliding in the cart drawer, hover effects on product cards, and Alpine.js x-transition transitions on modals or filters. A stuttering menu immediately feels unfinished and cheap, even if the page's actual load time is excellent. Animation performance is therefore not a cosmetic detail but a directly perceivable quality signal, independent of server response times or bundle sizes.

2. The rendering pipeline: layout, paint, and composite

After every style change, the browser potentially runs through four stages. First, style calculation determines which CSS rules apply to which element. Next comes layout (also called reflow): the browser computes the exact size and position of every element in the document. Because elements affect one another, a single size change can trigger a recalculation of the entire document tree, not just the affected element.

After that, paint rasterizes individual elements into pixels on separate layers, including colors, shadows, and rounded corners. The final stage, composite, combines these already-rasterized layers via the GPU into the final image, without touching layout or paint again. The crucial point: composite is orders of magnitude cheaper than layout and paint, because it only shifts, scales, or blends existing bitmaps instead of recomputing pixels. Any animation that runs exclusively at the composite stage therefore skips the two most expensive steps of the pipeline entirely.

3. Which CSS properties trigger layout, paint, or composite

CSS properties fall roughly into three cost classes. Layout-triggering properties such as width, height, top, left, margin, padding, or font-size force the browser to recompute geometry, and automatically drag paint and composite along with them, since the affected pixels necessarily move. For an animated left value, that means a full layout pass 60 times per second, potentially across the entire visible document tree.

A second class, paint-triggering properties such as color, background-color, box-shadow, or border-radius, skips layout but still forces a re-rasterization of the affected pixels on every frame. That's cheaper than layout but still more expensive than pure compositing, especially for large elements or elaborate shadows with a large blur radius. A particularly sneaky pattern is layout thrashing: if a JavaScript loop alternately writes (e.g. element.style.width) and reads (e.g. element.offsetWidth), every read forces a synchronous layout recalculation before the next frame is even due.

4. Why transform and opacity are almost free

transform and opacity belong to the third class: they trigger neither layout nor paint but are processed directly at the composite stage. The reason lies in the nature of the operations: a translation, scale, or rotation via transform only changes how an already-rasterized layer is positioned on screen, not which pixels that layer contains. The GPU can handle exactly that with a simple matrix multiplication, freshly every frame, without the CPU having to recompute anything.

opacity works analogously: the browser blends two already-existing layers using an alpha value instead of recoloring the underlying pixels. The prerequisite is that the browser has already promoted the animated element to its own compositor layer beforehand, which modern browsers do automatically when they detect transform or opacity transitions. In practice this means: a sidebar animated with transform: translateX() can run buttery smooth at 60fps, while the exact same visual movement via left noticeably stutters on a mobile device, even though the end state looks identical.

5. will-change: benefits and costs

The CSS property will-change tells the browser in advance that an element is about to change, so it can create the corresponding compositor layer ahead of time instead of only at the first animation frame. Without this hint, the first frame of an animation can noticeably stutter, because layer creation itself takes time. With will-change: transform on a hover element, that cold-start effect disappears.

The catch: every promoted layer consumes its own GPU memory, and with too many layers active at once, memory bandwidth can become the limiting factor, which degrades overall performance instead of improving it. Applying will-change blanket-style to every card in a product list is therefore a common anti-pattern. The proven practice: set will-change shortly before the animation starts, for example via JavaScript on the mouseenter event, and remove it again after completion via transitionend, rather than anchoring it permanently in the stylesheet.

6. Practical refactoring examples

The most common refactoring candidates in Magento and Hyvä themes are slide-in menus animated via left or right, and hover effects on product cards that change width or height. Both can be losslessly converted to transform: translateX() or transform: scale() respectively, without changing the visual result. The important part: the element must already sit at the correct base position before the animation, with movement arising purely from the transform offset.

In Hyvä themes, Alpine.js already uses opacity and transform by default via x-transition for enter and leave transitions, which is structurally sound. Still, it's worth auditing your own theme CSS: Tailwind utility classes like transition-all combined with hover classes that change w- or h- implicitly animate layout properties. Replacing them with scale- utilities or an explicit transition-transform class fixes this without any extra JavaScript.


/* Bad: animating "left" triggers a full layout pass on every frame */
.menu-slow {
  position: fixed;
  left: -280px;
  transition: left 0.3s ease;
}
.menu-slow.is-open {
  left: 0;
}

/* Good: animating transform runs entirely on the compositor thread */
.menu-fast {
  position: fixed;
  transform: translateX(-280px);
  transition: transform 0.3s ease;
}
.menu-fast.is-open {
  transform: translateX(0);
}

/* Bad: will-change applied permanently to every product card */
.product-card {
  will-change: transform, opacity;
}

/* Good: promote the layer only right before the animation runs */
.product-card {
  transition: transform 0.2s ease;
}
.product-card:hover {
  will-change: transform;
  transform: scale(1.03);
}
/* Remove the hint again via JS once the transition ends, e.g. on transitionend */

7. requestAnimationFrame for JS-driven animations

For animations that can't be expressed as a CSS transition or keyframe animation, such as physics-based motion or a custom-calculated scroll-to-top effect, requestAnimationFrame (rAF) is the right tool. Unlike setInterval or setTimeout, rAF syncs exactly with the next screen refresh and automatically pauses when the tab runs in the background, avoiding both jank and unnecessary power consumption.

Decisive for smooth rAF animations is strictly separating DOM reads from DOM writes: if the same loop first reads getBoundingClientRect() and then writes styles, layout thrashing results. Instead, all measurements should happen once before the loop, with each frame only writing transform or opacity. Using the timestamp parameter the browser passes to every rAF callback also allows a framerate-independent calculation of animation progress, instead of relying on a fixed number of frames.


// Smooth scroll-to-top driven by requestAnimationFrame, not setInterval
function animateScroll(targetY, duration) {
  const startY = window.scrollY;
  const distance = targetY - startY;
  const startTime = performance.now();

  function step(now) {
    const elapsed = now - startTime;
    const progress = Math.min(elapsed / duration, 1);
    // Ease-out cubic for a natural deceleration
    const eased = 1 - Math.pow(1 - progress, 3);
    window.scrollTo(0, startY + distance * eased);

    if (progress < 1) {
      requestAnimationFrame(step);
    }
  }

  requestAnimationFrame(step);
}

8. The FLIP technique for layout animations

Some effects, such as reordering a product list after a filter change, necessarily involve layout properties like position and size. The FLIP technique (First, Last, Invert, Play) resolves this dilemma by letting the layout change itself happen instantly and without animation, while masking the visual jump using transform. The sequence: first, the starting position is measured (First), then the DOM change is applied immediately and the new position is measured (Last), then the element is visually shifted back to the old position via transform (Invert), and finally animated to the final position (Play).

To the browser, the result looks like a real layout animation, but in fact only a transform animation runs on the compositor. Typical use cases in Magento stores are reordering product cards after a filter change, collapsing a cart line item after removing a product, or toggling between list and grid view on a category page, in each case without the browser having to recompute layout during the actual animation.


// FLIP: First, Last, Invert, Play for a reordering product grid
function flip(element, mutateDom) {
  const first = element.getBoundingClientRect();

  mutateDom();

  const last = element.getBoundingClientRect();
  const deltaX = first.left - last.left;
  const deltaY = first.top - last.top;

  // Invert: jump back to the old visual position using only transform
  element.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
  element.style.transition = 'transform 0s';

  requestAnimationFrame(() => {
    // Play: animate to the identity transform, layout stays untouched
    element.style.transition = 'transform 0.3s ease';
    element.style.transform = '';
  });
}

9. Measuring with Chrome DevTools: Rendering tab and Performance panel

Whether an animation actually runs only on the compositor can be verified precisely with Chrome DevTools, instead of assuming it. In the Rendering tab (accessible via the command menu with Show Rendering), Layout Shift Regions highlights unexpected layout changes in color, while Layer Borders makes visible which elements already have their own compositor layer. The Frame Rendering Stats overlay shows the current framerate live and whether GPU rasterization is active.

In the Performance panel, a recorded interaction provides the most reliable diagnosis: the timeline shows layout and paint events as their own colored bars, usually purple for layout and green for paint. If an animation runs cleanly on the compositor only, none of these bars appear for the entire duration of the animation, only the much narrower composite events. Chrome also explicitly flags forced synchronous layouts as Forced reflow in the timeline, usually with a red warning triangle, making layout-thrashing spots in your own code instantly discoverable.


/* Loading spinner and toast fade-in using compositor-only properties */
@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes fade-in-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

.spinner {
  animation: spin 0.8s linear infinite;
}

.toast {
  animation: fade-in-up 0.25s ease-out;
}

CSS properties compared side by side

The table below maps the most common animated CSS properties to the pipeline stage they trigger and shows which alternative is preferable in practice.

CSS property Triggered pipeline stage Cost Recommendation
width / height Layout + paint + composite High Use transform: scale()
top / left / margin Layout + paint + composite High Use transform: translate()
color / background-color Paint + composite Medium Only animate on small areas
box-shadow Paint + composite Medium Fade a pre-rendered shadow via opacity
transform Composite only Low First choice for movement, scale, rotation
opacity Composite only Low First choice for fade effects

In practice, almost all common UI animations, from slide-ins to modals to hover effects, can be implemented entirely with transform and opacity. For the rare effects that must involve layout properties, the FLIP technique helps mask the layout jump without an animated reflow.

Mironsoft

Animation performance and compositor optimization for Magento and Hyvä stores

Ready to get your animations to 60fps?

We analyze your store's animations with the Chrome Performance panel, identify layout thrashing and expensive properties, and refactor them onto transform and opacity, including Alpine.js transitions.

Animation audit

Performance panel analysis, identifying layout thrashing

Refactoring onto compositor layers

Switching to transform and opacity with zero visual change

Alpine.js transition tuning

Tuning x-transition and rAF animations for Hyvä interactions

10. Summary

Animation performance is decided in the rendering pipeline, not in the eye of the beholder. Animating width, top, left, or margin forces a full layout pass on every frame, while transform and opacity are processed exclusively at the cheap composite stage and thereby reliably hit 60fps. will-change speeds up the animation start, but should be used deliberately and temporarily so it doesn't needlessly burden GPU memory.

For JavaScript-driven animations, requestAnimationFrame syncs motion exactly with the screen refresh, while strictly separating DOM reads from writes prevents layout thrashing. When effects necessarily involve layout properties, such as reordering lists, the FLIP technique bridges the jump with a pure transform animation. Chrome DevTools, with its Rendering tab and Performance panel, provides the objective proof of whether an animation actually runs on the compositor or unknowingly triggers layout and paint.

Animation Performance - The Essentials at a Glance

Avoid expensive properties

width, top, left, margin trigger layout. Always use transform for movement and scale.

Prefer compositor-only

transform and opacity skip layout and paint, running directly on the GPU, for stable 60fps.

Use will-change deliberately

Set it shortly before the animation, remove it afterward. Applying it permanently to many elements is an anti-pattern.

Measure, don't guess

Chrome Performance panel and Rendering tab show layout, paint, and composite events per frame.

11. FAQ: Animation Performance with transform and opacity

1Why is animating width, height, top, or left slow?
Forces a full layout pass over the document tree on every frame, followed by paint and composite. This often blows the 16.7-millisecond budget per frame.
2What is the difference between layout, paint, and composite?
Layout computes size/position, paint rasterizes pixels onto layers, composite combines finished layers via the GPU. Composite is far cheaper than the other two stages.
3Why are transform and opacity faster than other properties?
Both run only at the composite stage. The GPU shifts or blends already-rasterized layers without recomputing pixels.
4How does will-change work and when should I use it?
Creates the compositor layer before the first animation frame. Set it shortly before the animation and remove it afterward, don't leave it permanently in CSS.
5What happens if I apply will-change to too many elements?
Every layer consumes its own GPU memory. Too many active layers turn memory bandwidth into a bottleneck and degrade performance.
6How do I animate a position change without triggering layout?
With FLIP: measure position before/after the DOM change, visually shift back via transform, then animate to the target position. Only a transform animation actually runs.
7When should I use requestAnimationFrame instead of a CSS transition?
For animations that can't be described by a simple start/end state, such as physics-based or custom-calculated curves. rAF syncs exactly with the screen refresh.
8How do I check with Chrome DevTools whether an animation runs on the compositor?
Record it in the Performance panel: if purple layout and green paint bars are absent during the animation and only narrow composite events appear, it runs purely on the compositor.
9What is the difference between CSS transitions and JS animations in Alpine.js?
Alpine x-transition defaults to CSS transitions with opacity/transform, performant and optimized by the browser. JS animations via rAF are only needed for more complex, non-linear curves.
10Can I animate box-shadow performantly?
box-shadow triggers paint, more expensive than transform/opacity, but cheaper than layout properties. For frequent animations, better to fade a pre-rendered shadow element via opacity.