CSS will-change: Use It Wisely or Risk a Memory Leak
AI generated
CSS · Performance · GPU · Memory
CSS will-change
use it wisely or risk a memory leak

will-change promises smoother animations through GPU layer promotion, and it delivers on that promise when used correctly. Applied permanently to too many elements, though, it causes memory leaks, increases GPU memory usage, and can actually make animations slower instead of faster.

13 min read will-change · transform hack · layer promotion · memory leak · compositor All modern browsers · Chrome DevTools

1. What will-change promises and what it actually does

will-change is a CSS property that lets developers tell the browser which properties of an element are about to change. This allows the browser to perform optimizations ahead of time, most notably moving the element onto its own GPU compositor layer. The idea behind it: if the browser knows an element is about to be animated, it can build the necessary infrastructure before the animation starts instead of waiting for the first frame. That prevents the first-frame stutter that would otherwise occur without this pre-optimization.

What will-change actually does depends on the browser. In practice, will-change: transform triggers a layer promotion in every modern browser: the element gets its own compositor layer and is managed as a separate image layer on the GPU. Animations of transform and opacity can then run entirely on the GPU compositor thread without blocking the JavaScript main thread. That is the core benefit, but it comes at a price: every GPU layer consumes graphics memory, CPU time for layer management, and increases the memory footprint of the rendering process.

2. Layer promotion: how the browser creates GPU layers

The browser normally keeps all DOM elements in a single rendering context and paints them together. Layer promotion means an element gets moved into its own "layer" that can be composited independently of the rest of the document. Several CSS properties trigger this process automatically: transform (when animated), opacity (when animated), filter, will-change with certain values, and position: fixed. isolation: isolate and contain: paint can also trigger layer promotion.

Layer promotion has an important side effect: it creates a new stacking context. That explains why adding will-change: transform or transform: translateZ(0) sometimes causes unexplainable layout shifts, elements that previously sat in one stacking context suddenly find themselves in a new one. This interaction between layer promotion and stacking contexts is one of the most common sources of unexpected side effects when adding will-change.


/* Layer promotion mechanisms: each creates a compositor layer */

/* EXPLICIT: will-change directly signals intent to the browser */
.animated-card {
  will-change: transform; /* Promotes to GPU layer immediately */
}

/* IMPLICIT: Browser auto-promotes when these properties animate */
.auto-promoted {
  transition: transform 0.3s ease; /* Promoted only during animation */
  /* No permanent layer until transition starts */
}

/* The transform-hack: forces permanent promotion */
.hack-promoted {
  transform: translateZ(0); /* GPU layer even without animation */
  /* Side effect: creates new stacking context! */
}

/* CORRECT: will-change only on elements that actually animate frequently */
.modal-overlay {
  will-change: opacity; /* Modal opens/closes often, promotion justified */
}

/* WRONG: promoting static elements wastes GPU memory */
.static-heading {
  will-change: transform; /* Never animates, pure memory waste */
}

/* Layer cost check: Chrome DevTools → Rendering → Layer Borders
   Red borders = compositor layers. Too many = problem. */

3. Memory cost: why too many layers hurt

Every GPU compositor layer costs graphics memory proportional to the pixel size of the element. An element of 400×300 pixels on a Retina display (2x) uses 400 × 300 × 4 bytes × 4 (2x²) = ~1.9 MB of GPU memory. Multiply that by 50 elements that all have will-change: transform, and you get roughly 95 MB of GPU memory usage, just for layer management. On mobile devices with shared CPU/GPU memory (unified memory architecture), that can lead to memory pressure, jank and, in the worst case, browser crashes.

The memory leak pattern with will-change looks like this: you add will-change: transform in the static CSS "to prepare for animations" and never remove it again. The browser keeps the layer around permanently, even after the animation has finished, even when the element is outside the viewport. This is not a memory leak in the classic sense (the layer is released when the element is removed from the DOM), but it is a permanent, unnecessary memory overhead that degrades performance across the entire page. The correct approach is to add will-change right before the animation and remove it immediately after it finishes.

4. The transform hack: translateZ(0) and will-change:transform

Before will-change was introduced, the common trick for GPU acceleration was transform: translateZ(0) or transform: translate3d(0,0,0). Both force a layer promotion because the browser always places a 3D-transformed element onto a GPU layer. This hack was legitimate at a time when there was no official API for layer promotion. Today it is an antipattern: it creates the same layer overhead as will-change but communicates no intent, the browser has no way to use that information to release the layer once it is no longer needed.

Even more problematic: translateZ(0) has an actual geometric effect, it changes the element's z-position in 3D space. That can have unexpected effects on stacking order, perspective-based parent systems, and transform-style: preserve-3d containers. will-change: transform, on the other hand, communicates pure intent with no geometric side effect. If you do need to force layer promotion, which should rarely be necessary, will-change is always the cleaner choice over the transform hack.


/* ANTIPATTERN: transform hack on every card, GPU memory waste */
.product-card {
  transform: translateZ(0); /* Forces layer on all 48 product cards */
  /* 48 cards × ~2MB GPU = ~96MB just for layer overhead */
}

/* ANTIPATTERN: will-change on everything */
* {
  will-change: transform; /* Absolute worst practice, promotes entire DOM */
}

/* ANTIPATTERN: permanent will-change in CSS for rarely-animated elements */
.hero-banner {
  will-change: opacity; /* Animated once on page load, never again */
  /* Layer promoted permanently: memory wasted 99% of the time */
}

/* CORRECT: dynamic will-change via JavaScript */
/* In CSS: no will-change on static state */
.animated-card {
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

/* In JavaScript: set before interaction, remove after */
card.addEventListener('mouseenter', () => {
  card.style.willChange = 'transform';
});
card.addEventListener('mouseleave', () => {
  card.style.willChange = 'auto'; /* Reset, layer released */
});

5. When will-change actually makes sense

The official CSS specification names three legitimate use cases for will-change. First, elements that are animated very frequently: menus that open and close on every hover, modals that appear regularly, or a permanently visible animated loader. Second, elements with expensive animations where the first frame would stutter without layer promotion, for example a large hero image with a complex CSS filter transition. Third, elements where a measurable performance improvement after adding will-change can actually be proven in the profiler.

The key rule is this: will-change is the last optimization step, not the first. The first step is to animate only compositor-friendly properties (transform and opacity) instead of width, height, top, left or margin, these properties trigger layout calculation on the main thread and cannot run on the compositor thread no matter what will-change promises. will-change cannot avoid a layout recalculation, it only optimizes the compositing phase.

6. Setting and removing will-change dynamically

The correct pattern for will-change is dynamic: it is not set in the CSS stylesheet at all. Via JavaScript, it is added shortly before an animation starts, ideally on mouseenter, touchstart, or right before a fetch request delivers the data that will trigger an animation. Once the animation finishes, it is removed immediately with element.style.willChange = 'auto'. The auto value is the default and signals to the browser that no further pre-optimization is needed, the layer gets released.

For animations triggered via CSS transitions, you can set will-change in the transitionstart event and remove it in the transitionend event. For CSS keyframe animations, use animationstart and animationend accordingly. The result: the layer only exists for the duration of the animation, no permanent memory overhead, no layer bloat on pages with many elements. This approach requires a few lines of JavaScript, but the memory savings on mobile devices are worth it.


/* Pattern: will-change only during animation, set/remove via JS events */

/* CSS: no will-change in static state */
.animated-element {
  transform: translateX(0) scale(1);
  opacity: 1;
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
              opacity 0.3s ease;
}

.animated-element.is-entering {
  transform: translateX(20px) scale(0.97);
  opacity: 0;
}

/* JavaScript: dynamic will-change lifecycle */
/*
const el = document.querySelector('.animated-element');

// Set will-change just before animation starts
el.addEventListener('mouseenter', () => {
  el.style.willChange = 'transform, opacity';
});

// Remove after animation completes, layer is released
el.addEventListener('transitionend', () => {
  el.style.willChange = 'auto';
}, { once: false });

// For keyframe animations:
el.addEventListener('animationstart', () => {
  el.style.willChange = 'transform';
});
el.addEventListener('animationend', () => {
  el.style.willChange = 'auto';
});
*/

/* Special case: scroll-driven animations, permanent layer IS justified */
.sticky-nav {
  will-change: transform; /* Always in viewport, always compositing scroll */
  position: sticky;
  top: 0;
}

7. The most common will-change antipatterns

The most common will-change antipattern is applying it globally: * { will-change: transform; } or * { will-change: auto; } in a base CSS file. The former promotes every single DOM node to its own GPU layer, GPU memory usage explodes, and on mobile devices this leads to an immediate performance drop. The latter is harmless but pointless. Another common antipattern is setting will-change in a CSS hover state. It technically works, but the browser only gets the hint once the hover state is triggered, which is exactly when the animation starts. That is too late for "pre-optimization".

A subtler antipattern is will-change: contents. This declaration signals that the element's content changes regularly, for example with live-updating content. The browser responds by preparing the sub-tree elements for independent layer promotion. That sounds useful but is rarely practical, because "contents" is too vague and pushes the browser toward overly aggressive optimizations that do not necessarily match the actual change frequency. will-change: scroll-position applied to too many containers at once also creates unnecessary layer overhead.

Scenario Set will-change? When to remove? Risk
Modal dialogs (frequent) Yes, permanently Never (justified) Low, 1 layer
Product card grid (48 items) No / dynamic After mouseleave High if permanent
Sticky navigation Yes, permanently Never Low, 1 layer
Hero banner (one-time animation) Yes, temporarily After animationend Low if done correctly
Static headings No n/a Pure memory waste

9. Alternatives and diagnostic tools

Before reaching for will-change, make sure the animation is actually using compositor-friendly properties. transform and opacity can be animated on the compositor thread without needing will-change at all, the browser automatically promotes the element once these properties become active via a CSS transition or animation. Properties like width, height, margin, padding or top/left, on the other hand, trigger layout recalculation on the main thread, and no amount of will-change can prevent that.

Chrome DevTools offers several tools for diagnosing layers. In the Rendering panel (DevTools → More tools → Rendering), you can enable "Layer Borders", which marks every compositor layer with a colored border. Too many borders on a page indicate layer proliferation. The Performance panel shows GPU memory usage over the timeline. The "Layers" panel (accessible from the DevTools menu) shows a 3D view of all active compositor layers and their memory usage. With these tools you can measure precisely whether will-change is actually helping or hurting a given page.

Mironsoft

CSS performance, rendering optimization and Hyva theme development

Need to fix CSS performance issues systematically?

We analyze layer proliferation, memory leaks and animation jank in Magento and Hyva projects using Chrome DevTools, and fix performance issues at the root, instead of papering over them with will-change.

Performance audit

Layer analysis, will-change inventory and GPU memory profiling of your project's CSS

Animation optimization

Jank-free animations with compositor-friendly properties and correct will-change usage

Mobile optimization

GPU memory reduction for iOS and Android, especially critical on unified memory devices

10. Summary

CSS will-change is a powerful but frequently misused tool. It tells the browser which CSS properties of an element are about to change, enabling upfront layer promotions for smoother animations. Used incorrectly, applied permanently to too many elements or thrown at every animation performance question, it does more harm than good: memory leaks, GPU overload and, paradoxically, worse performance. The transform hack (translateZ(0)) is an antipattern today, will-change communicates the same intent without geometric side effects, and setting/removing it dynamically is the cleanest solution.

The practical rule: only apply will-change after profiler evidence. First, does the element animate transform or opacity? (If not, will-change will not help.) Second, does the animation measurably stutter in the profiler? Third, does the animation improve in the profiler after adding will-change? Only if all three answers are "yes" is will-change justified, and even then, ideally set dynamically via JavaScript rather than permanently in the stylesheet.

CSS will-change: the essentials at a glance

Profiler evidence only

Only apply will-change after measurable jank in the performance profiler. Do not add it prophylactically to elements that "might animate someday".

Set it dynamically

Set it via JS shortly before the animation, remove it with willChange = 'auto' after animationend/transitionend. Prevents permanent layer overhead.

No transform hack

translateZ(0) is an outdated antipattern. will-change: transform communicates the same intent without geometric side effects.

Compositor properties

Only transform and opacity benefit from layer promotion. Animating width, height, top, left triggers layout, and will-change will not help.

11. FAQ: CSS will-change

1What does CSS will-change do?
Signals to the browser which properties are about to change. The browser can perform layer promotion onto the GPU compositor ahead of time, for smoother animations.
2Does will-change cause memory leaks?
Every GPU layer consumes memory proportional to its pixel size. Many elements permanently promoted means memory overhead with no benefit. Setting and removing it dynamically avoids this.
3will-change:transform vs. translateZ(0)?
translateZ(0) has geometric side effects in 3D space. will-change:transform only communicates intent. Always prefer will-change, the transform hack is outdated.
4When to set it permanently in CSS?
Only for very frequently animated elements: sticky nav, modal dialogs, a permanent spinner. Rarely animated: set it dynamically via JS and remove it after the animation.
5Does will-change help with width/height?
No. width/height trigger layout recalculation on the main thread. will-change only optimizes the compositor phase, only transform and opacity benefit, not layout properties.
6Remove will-change after an animation?
element.style.willChange = 'auto' in the transitionend or animationend event. 'auto' means normal browser heuristics, the layer gets released.
7How many elements are too many?
No fixed number. Rule of thumb: max 3 to 5 permanently promoted. Lists: set dynamically. Layer Borders in DevTools reveal the problem immediately.
8Diagnose layer proliferation?
Chrome DevTools → Rendering → Layer Borders. The Layers panel shows memory per layer. The Performance panel shows GPU memory over time. Too many borders means a problem.
9Not setting it in the :hover state?
Set in :hover, it arrives too late for pre-optimization. A mouseenter event in JavaScript arrives earlier, the layer is ready before the first animation frame.
10What does will-change: auto mean?
The default value: 'no special pre-optimization'. The browser decides on its own using heuristics. It does not mean 'do nothing', it means 'normal browser intelligence'. Use it to reset after animations.