will-change and Layer Hints
will-change hints to the browser that an element is about to change, so it can promote that element to its own compositor layer ahead of time instead of paying a one-frame stall mid-animation. This is part of Layout and Paint Optimization, and pairs directly with animation performance patterns where compositor-thread animation is the goal.
The diagram shows the promotion: without the hint an element shares the root layer and repaints with its siblings; with the hint it gets a dedicated backing texture the compositor can transform independently of the main thread.
What will-change Does
will-change is a CSS property that signals to the browser that an element is about to change in a specific way, allowing it to allocate GPU resources (a dedicated compositor layer and pre-rasterized texture) before the animation starts. This eliminates the one-frame latency penalty that occurs when the browser promotes an element mid-animation.
The cost is a persistent GPU texture allocation for as long as will-change remains active. On a desktop with gigabytes of VRAM, this is negligible. On a mobile device with 256β512MB of shared GPU/CPU memory, applying will-change to every animated element simultaneously can exhaust the budget and trigger texture eviction, causing the very jank it was meant to prevent.
will-change is most effective when:
- Applied just before an animation starts.
- Removed as soon as the animation ends.
- Limited to the specific properties that will change (
transform,opacity), not used as a broad hint.
The payoff is timing. Without the hint the compositor promotes and rasterizes the layer inside the first animated frame, blowing the budget once; with the hint that work happens while the element is idle, so every animated frame is a cheap composite.
Identifying Misuse
Anti-patterns that cause problems within the Layout and Paint Optimization pipeline:
will-change: transformapplied statically in a stylesheet to every card, list item, or button β promoting hundreds of elements simultaneously.- Leaving
will-changeactive after a transition completes, retaining GPU textures indefinitely. - Using
will-changeon layout-triggering properties (width,top,padding) β these cannot be compositor-only, so the hint provides no benefit and still allocates the texture. Prefer the compositor-safe pair instead; see why transform and opacity are GPU-accelerated.
Key indicators in traces:
UpdateLayerTreeduration > 2ms β the compositor is reconciling a large or frequently-changing layer tree.- GPU memory spikes correlating with DOM node count β static
will-changeon many elements. Layoutevents immediately beforePaintβ suggests layout thrashing upstream of the paint invalidation.
Most misuse collapses to three questions about how the hint was applied. Walk each element through the decision below before it ships; when a static rule promotes hundreds of nodes, count the resulting layers directly to confirm the blowup before you refactor.
For the memory implications of layer over-promotion, see When to use will-change without memory leaks.
Trace Analysis
Profile with chrome://tracing (categories: cc, blink) or the DevTools Performance panel. Filter for Layerize, Rasterize, and Composite event chains.
[Main Thread] 14.2ms | Layout: Forced synchronous layout (read/write interleave)
[Main Thread] 1.8ms | UpdateLayerTree: promoted 12 layers via will-change
[Compositor] 0.4ms | Layerize: allocated GPU texture for layer #0x7F9A
[Compositor] 2.1ms | Rasterize: full tile raster (cache miss β layer just promoted)
[Compositor] 0.9ms | Composite: swap buffers
Total: 19.4ms β FRAME BUDGET EXCEEDED
The 14.2ms forced layout is the primary problem here. The will-change promotion adds overhead on top. The cache miss on rasterization (because the layer was just promoted) adds further cost. All three problems interact.
Read the same trace as two swimlanes: the main thread pays for the forced layout and layer-tree update, then the compositor thread pays for the fresh raster before it can swap buffers. The stacked total crosses the 16.7ms budget line.
Mitigation
Every safe pattern below shares one shape: attach the hint on an interaction signal, let the compositor run the animation off the main thread, then release the texture on the completion event. The lifecycle keeps exactly one promoted layer alive for the duration of the interaction and no longer.
Dynamic hint injection
Apply and remove will-change programmatically around the animation lifecycle:
function applyScopedLayerHint(element, property) {
if (element.style.willChange === property) return
element.style.willChange = property
// Remove after the animation completes to free the compositor layer
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto'
}, { once: true })
}
For animations triggered by hover or focus, CSS is cleaner:
.card {
transition: transform 0.2s ease;
}
/* Hint applied only during the interaction window */
.card:hover,
.card:focus-within {
will-change: transform;
}
CSS containment as a complement
.list-item {
contain: strict; /* isolates layout and paint for the item */
}
contain: strict restricts the scope of layout and paint invalidations without allocating GPU memory. Use it on virtualized list items and other high-frequency mutation targets as a complement to β not a substitute for β will-change. See CSS Containment Strategies.
Framework patterns
- React: Use
useTransitionfor state changes that trigger animated transitions. Apply a CSS class withwill-changeduring the transition and remove it in the cleanup effect. For the interaction with concurrent rendering, see React concurrent rendering vs forced reflow. - Vue: Use
v-bindwith a computed property that setswill-changeonly when the component is in an animating state. - Vanilla:
IntersectionObserverto scope promotion to visible elements;animationend/transitionendto tear down.
Validation
Success criteria:
- Sustained <16ms frame duration at 95th percentile
- Zero persistent GPU layer allocations after interaction ends
- Heap size delta < 5% after component lifecycle completes
- Automated Performance traces show no UpdateLayerTree spikes > 2ms at idle
Run Lighthouse CI focusing on INP and CLS. A CLS regression after adding will-change hints at layout being affected by the promotion; verify with a Performance trace.
Gate the change on all four checks passing. Any single failure sends the change back to mitigation rather than to production.
In This Section
- When to use will-change without memory leaks β scoping the hint so promoted textures are always released.
- Measuring Compositor Layer Count in DevTools β reading the Layers panel to confirm how many layers a hint actually created.
What will-change Actually Does
will-change is a hint that tells the browser you are about to change a property, so it can prepare β most usefully by promoting the element to its own compositor layer ahead of time, so the first frame of an animation does not pay the cost of layer creation. will-change: transform and will-change: opacity are the values that matter, because they let the engine rasterize the element into a texture in advance and then animate it on the compositor without re-entering layout or paint. Used this way, right before an animation, it removes the one-frame hitch that comes from promoting a layer mid-animation. The mechanics of doing it safely are in promoting layers safely with translateZ, which contrasts the modern hint with the older translateZ(0) hack.
The critical word is hint. The browser is free to ignore will-change β under memory pressure, or on engines like WebKit that are conservative about promotion β so it is not a guarantee, and code should degrade gracefully when the layer is not created. It is also not a performance switch to sprinkle broadly: each promoted element costs GPU texture memory, and a will-change on a rule that matches many elements promotes all of them, which is a fast route to exhausting the memory budget and triggering eviction stalls. The value is precision β promote the one element that will animate, at the moment it will animate.
The Cardinal Rule: Add Late, Remove Early
The single most important discipline with will-change is that it should be transient. Setting it permanently in a stylesheet keeps the elementβs texture resident for the entire life of the page, consuming memory whether or not the element is animating and stealing budget from content that needs it. The correct pattern is to add the hint in JavaScript just before the animation starts β or on an interaction that reliably precedes it, like mouseenter on a hover-animated element β and remove it once the animation settles, returning the memory. An element that animates once on load has no business carrying will-change for the rest of the session.
This is also why will-change is the wrong first response to jank. If an animation stutters, the cause is usually a forced reflow or an expensive paint upstream, and promoting a layer only relocates that cost while adding memory pressure. The order that works is to fix the layout or paint problem first, using the techniques in reflow and repaint triggers, and reach for a layer hint only once the remaining per-frame cost is genuinely the transform of a specific moving element. Verifying the result in the Layers panel β one intended layer, reasonable memory, removed after the animation β is what separates a deliberate optimisation from a leak.
// Add the hint right before the animation, remove it when the animation ends.
el.style.willChange = 'transform'
const anim = el.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(200px)' }],
{ duration: 300, easing: 'ease-out' },
)
anim.finished.then(() => { el.style.willChange = 'auto' }) // release the texture
Auditing for Stale Hints
On a mature codebase the most common will-change problem is not a missing hint but a stale one β a hint added for an animation that no longer runs, a translateZ(0) copied from an old fix whose cause is long gone, or a will-change left in a stylesheet where it promotes every matching element permanently. Each of these holds GPU texture memory for motion that never happens, and on memory-constrained devices that wasted budget is the difference between a scroll that holds its frame rate and one that stutters as textures are evicted. A periodic audit of the Layers panel surfaces them: any layer whose element is not currently animating, and whose compositing reason is a will-change or a translateZ hack, is a candidate for removal.
The audit is cheap and the payoff is real. Removing a stale hint frees its texture with no visible change to the page, returning budget to the elements that genuinely need it. Building the check into a routine β a sweep of the Layers panel when touching a component, or a lint rule that flags will-change in static CSS β keeps hints from accumulating. The guiding principle is that will-change is a verb, not an attribute: it describes an imminent change, so it should exist only around the moment of that change and be removed the instant it is no longer imminent. Treat it that way and it stays the precise, temporary optimisation it was designed to be rather than a slow leak of GPU memory. A good heuristic for whether a hint is earning its keep is to ask what animation it is preparing for and when that animation runs: if you cannot name the imminent change, the hint should not be there. That question also guides where to add it β on the specific element that will transform, not a convenient ancestor β and when to remove it, namely as soon as the animation it prepared for has finished.
Frequently Asked Questions
Does will-change speed up an animation once it is already running?
No. will-change only affects the setup cost β it moves layer promotion and the first rasterization off the animated frame path so there is no mid-animation stall. Once the layer exists and the animation is running on the compositor, the property has no further effect. Its whole value is paying the promotion cost during idle time instead of inside frame one.
Why is applying will-change to every card in a stylesheet harmful?
A static will-change: transform promotes every matched element to its own compositor layer immediately, and each layer holds a persistent GPU texture. On a phone with 256β512MB of shared memory this exhausts the budget and forces texture eviction, which produces the exact jank the hint was meant to prevent. Scope the hint to the elements actually animating and remove it when they stop.
Should I use will-change on properties like width or top?
No. width, top, and padding trigger layout, so they can never run compositor-only. The hint still allocates a texture but delivers no benefit because the animation must return to the main thread every frame. Animate transform and opacity instead, which the compositor can handle without layout or paint.
How do I confirm will-change actually created a layer?
Open the DevTools Layers panel and count the compositor layers before and after the hint applies. A correct scoped hint adds one layer during the interaction and returns to baseline afterward; a static rule shows a layer per matched node. See Measuring Compositor Layer Count in DevTools for the full workflow.
Is CSS containment a replacement for will-change?
No β they solve different problems. contain: strict limits the scope of layout and paint invalidations without allocating GPU memory, while will-change pre-promotes a layer at the cost of a texture. Use containment on high-frequency mutation targets like virtualized list items as a complement, not a substitute, for a scoped will-change hint.
Related Guides
- When to use will-change without memory leaks β the memory discipline behind every scoped hint.
- Measuring Compositor Layer Count in DevTools β verify how many layers a hint really produced.
- CSS Containment Strategies β scope invalidations without allocating GPU textures.
- Why transform and opacity are GPU-accelerated β the compositor-safe properties worth hinting.
- Layout and Paint Optimization β the parent guide for paint and compositing work.