Auditing will-change Usage in a Codebase
will-change is a promise that something is about to change, and the browser reserves a layer against it. A promise that is never kept is a standing allocation for no benefit — and because the declaration looks like an optimisation, it accumulates rather than being questioned. This guide is a short audit: what to search for, how to classify what you find, and what to do with each category. It sits under compositor-safe properties and will-change.
When to use this approach
- A layer figure that is higher than the effect count explains. Standing hints are one of the two main causes, alongside overlap promotion.
- An older codebase. Promotion hacks predate
will-changeand are still present in most projects of any age. - Before adding effects to a page that is near its budget. Reclaiming capacity from hints that do nothing is the cheapest headroom available.
- After inheriting a project. The hints in it were added for reasons that may no longer exist and are rarely documented.
Implementation
1. Search for more than the property
rg -n "will-change" --type css --type scss
rg -n "translateZ\(0\)|translate3d\(0" --type css --type scss
rg -n "backface-visibility:\s*hidden" --type css --type scss
rg -n "willChange" --type js --type ts
The legacy hacks force promotion just as effectively and will not appear in a search for the modern property. In most codebases they outnumber it.
2. Classify each declaration
For every hit, ask two questions. Does the element already run an animation on the hinted property — if so the hint is redundant, because a running animation promotes the element anyway. And can the selector ever stop matching — if not, the allocation is permanent.
Those two questions sort every declaration into the four categories above without needing to understand the history of any of them.
3. Delete the redundant and the stale
/* redundant — the animation already promotes it */
.reveal {
will-change: transform; /* delete */
animation: rise linear both;
animation-timeline: view();
}
Deleting a redundant hint changes nothing about behaviour and removes nothing but the allocation, which makes these the safest edits in the audit.
4. Scope the permanent ones
/* before */
.card { will-change: transform; }
/* after */
.card:hover,
.card:focus-within { will-change: transform; }
The hint’s value is promoting slightly ahead of an animation starting. Scoping it to the interaction that precedes the animation keeps that value and drops everything else.
5. Handle the imperative ones
el.style.willChange = 'transform';
requestAnimationFrame(() => el.animate(frames, opts));
animation.finished.finally(() => { el.style.willChange = ''; });
A hint applied in script must be removed in script. The finally handler is the part that gets omitted, which turns a scoped hint into a permanent one at runtime where no CSS search will find it.
Verification
Measure the layer figure before and after the audit on the page with the most hints. The reduction is usually immediate and satisfying, and it confirms that the declarations you deleted were doing what you thought.
Then confirm nothing regressed. Interact with each element whose hint you scoped and watch for a first-frame stutter — that is the promotion delay the hint existed to remove, and its return means the scope is too tight rather than that the hint was needed permanently.
Run the searches again after the edits. Imperative hints in particular tend to hide in files that the first pass did not cover.
Edge cases and gotchas
A hint on a parent that promotes children. will-change on a container can create a stacking context and change what is promoted beneath it. Removing it can change more than the container’s own layer.
Hints in third-party CSS. Component libraries add them, and they cannot be edited directly. An override that resets will-change: auto on the specific elements is the practical response, applied after the library’s own styles.
Hints on elements that animate rarely but critically. A modal that must open without a stutter is a legitimate case for a hint scoped to a class applied just before opening — which is a scoped hint, just scoped by state rather than by pseudo-class.
will-change with multiple values. Each named property may add its own constraint, and will-change: transform, opacity, filter can be more expensive than any one of them. Name only what actually changes.
Removing a hint that was masking a real problem. If deleting a hint reintroduces jank, the underlying animation is doing something expensive. The hint was hiding it, and the fix belongs in the animation.
Browser-specific notes
All three engines honour will-change and all three promote on running transform animations, so the redundancy rule holds everywhere.
Chromium’s Layers panel reports the promotion reason, which makes the classification step almost mechanical — a layer attributed to a will-change hint on an element with no running animation is a declaration to scope or delete.
Safari’s Web Inspector reports composited elements with less detail but enough to spot the same pattern. Firefox has no layer viewer, which is why the audit is done in Chromium; the deletions benefit every engine equally.
The legacy hacks behave the same everywhere and are equally unnecessary everywhere. There is no engine that still requires translateZ(0) to promote an element that a modern hint or a running animation would promote.
Frequently Asked Questions
Is will-change ever the right permanent choice?
Almost never. The closest legitimate case is a single element that animates on nearly every interaction and whose first frame must be perfect — a drag handle in an editor, for example. Even there, applying the hint on pointer-down is usually early enough, and it is the version that does not hold a layer while the tool is idle.
How much does one standing hint actually cost?
The element’s painted area times four bytes times the square of the device pixel ratio. For a small card that is under a megabyte on a desktop and a few megabytes on a phone. The problem is rarely one hint; it is a class rule matching thirty elements, which is where the same arithmetic becomes a real number.
Should the audit be automated?
The search should be; the classification should not. A lint rule that flags will-change on a selector with no interaction pseudo-class catches the permanent category automatically and is worth adding. Deciding whether a specific hint is redundant needs context about what else the element does.
What if removing hints makes the first frame stutter?
That is the promotion delay, and it means the hint was doing its job. The fix is to scope it to something that happens slightly earlier — hover before click, focus before activation — rather than to restore it permanently. Where nothing happens earlier, a permanent hint on that one element is a reasonable exception.
How often is this worth repeating?
Once a year on a stable codebase, and after any major dependency upgrade that touches components with animation. Hints accumulate slowly and through ordinary work — someone sees a stutter, adds a hint, the stutter goes away, and the hint stays after the underlying cause is refactored out.
The audit is short enough that attaching it to an existing rhythm works better than scheduling it: the release where you upgrade the component library, or the quarter where you review performance budgets generally.
Should I add a lint rule instead?
Add both. A rule that flags will-change on a selector with no interaction pseudo-class catches the permanent category as it is written, which is far cheaper than catching it a year later. It will not catch the legacy hacks or the imperative assignments, which is why the manual pass still has a job — but it will stop the largest category from growing between passes.
Related
- Compositor-Safe Properties & will-change — what promotion does and when it helps
- Reducing layer count on scroll-heavy pages — the other four interventions
- Counting composited layers in DevTools — measuring the result
- Chromium compositor behaviour for scroll timelines — reading promotion reasons