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.
Table of Contents
- 1. What will-change promises and what it actually does
- 2. Layer promotion: how the browser creates GPU layers
- 3. Memory cost: why too many layers hurt
- 4. The transform hack: translateZ(0) and will-change:transform
- 5. When will-change actually makes sense
- 6. Setting and removing will-change dynamically
- 7. The most common will-change antipatterns
- 8. will-change scenarios compared
- 9. Alternatives and diagnostic tools
- 10. Summary
- 11. FAQ
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.