will-change and Layer Hints
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.
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.
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.
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.
Mitigation
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. - 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.