@supports Guard Recipes for Performance-Sensitive Effects
A feature query answers whether a browser can run an effect. On a page with a dozen effects, the more consequential question is whether a given device should be asked to run all of them at once β and CSS has no query for that. This guide covers the conditions that come closest, which of them are honest proxies and which are misleading, and the cascade order that makes capability, cost and preference combine correctly. It sits under the engine support matrix.
When to use this approach
- A page carrying several simultaneous effects. One effect is rarely the problem; six on one screen frequently are, and only on some devices.
- Media-heavy sections. Where the cost is texture memory rather than computation, the constrained devices are exactly the ones with the smallest budget.
- After a real-device test found a problem. A cost limit is a response to a measurement, not a precaution to apply speculatively.
Skip it entirely on pages with one or two modest effects. A cost limit that never triggers is code that will drift out of correctness unnoticed.
Implementation
1. Keep the capability guard doing its own job
.reveal { opacity: 1; } /* baseline */
@supports (animation-timeline: view()) {
.reveal {
opacity: 0;
animation: fade-in linear both;
animation-timeline: view(block);
}
}
The feature query decides whether the effect exists. It should never also be carrying a cost decision, because the two change independently.
2. Choose an honest cost proxy
/* the display genuinely cannot repaint quickly β the most honest signal */
@media (update: slow) {
.reveal { animation: none; opacity: 1; }
}
/* small viewports correlate well enough with constrained devices */
@media (max-width: 40rem) {
.stack .card:nth-child(n + 4) { animation: none; }
}
update: slow is the only media feature that describes a rendering constraint directly. Everything else is correlation, and viewport width is the correlation that holds up best in practice.
3. Reduce the count, not the quality
@media (max-width: 40rem) {
/* keep the first three reveals; drop the rest to static */
.grid > :nth-child(n + 4) { animation: none; will-change: auto; }
}
Reducing how many things animate at once is far more effective than making each animation simpler, because the cost is dominated by simultaneously promoted layers rather than by keyframe complexity.
4. Order the conditions so preference wins
/* 1 baseline, 2 capability, 3 cost, 4 preference β in this order */
@media (prefers-reduced-motion: reduce) {
.reveal,
.stack .card {
animation: none;
opacity: 1;
scale: 1;
will-change: auto;
}
}
Placing the reduced-motion block last β or in a later cascade layer β is what guarantees it wins in all four combinations of capability and cost. Placing it earlier produces a page that honours the preference on unsupported engines and ignores it on supported ones.
5. Consider content-visibility before cutting effects
.section {
content-visibility: auto;
contain-intrinsic-size: auto 60vh;
}
Skipping rendering for off-screen sections reduces the peak layer count without removing anything the reader will actually see, which is a better trade than disabling effects outright.
Verification
Confirm each condition independently. Emulate update: slow in devtools where available, resize below the cost breakpoint, and set the reduced-motion preference β checking after each that the page is in the state you intended rather than in an unexpected combination.
Then test the combinations that matter: supported plus reduced motion, and small viewport plus reduced motion. These are the two that reveal ordering mistakes, and they are the two nobody checks.
Finally, measure. A cost limit that does not measurably improve the frame timing or the layer count on the device it was written for is complexity without benefit, and should be removed rather than kept as insurance.
Edge cases and gotchas
update: slow is rarely reported. Most devices report fast, including plenty that struggle. It is honest when it fires and it fires seldom, so it cannot be the only cost condition.
Viewport width is not device capability. A small window on a fast desktop matches a mobile breakpoint. This is acceptable β the effect is merely reduced, not broken β but it is worth knowing that the condition is a proxy rather than a fact.
Device pixel ratio as a proxy. High ratios appear on both flagship phones and cheap ones, and on high-end laptops. As a capability signal it is close to noise, even though it is genuinely relevant to memory cost.
prefers-reduced-data. Where supported it is a reasonable additional condition for media-heavy effects, since a visitor asking for reduced data is unlikely to want a preloaded image sequence.
A cost limit that hides content. Reducing animation must never reduce what is readable. Anything the cost branch disables has to leave the content in its final visible state, exactly as the reduced-motion branch does.
Browser-specific notes
@supports behaves identically across engines and needs no special handling. The media features do not.
update is supported in all three engines but reported conservatively, so treat a slow result as a strong signal and a fast result as no signal at all.
prefers-reduced-data has partial support and is worth using as an enhancement rather than depending on it. Where it is absent the page simply does not take the extra reduction, which is a safe failure.
prefers-reduced-motion is the oldest and most reliably supported of all of them, predating every timeline API by years β which is why the reduced-motion branch needs no capability guard of its own and can safely be written as the final word.
Frequently Asked Questions
Can I detect device memory from CSS?
No. navigator.deviceMemory exists in some engines and is coarse, and using it to branch CSS means writing the decision in JavaScript and applying a class β which works and is a maintenance cost. For most sites, reducing the effect count on small viewports achieves the same practical outcome without the plumbing.
Should the cost branch be a separate stylesheet?
Not usually. Keeping it adjacent to the effect it limits makes the relationship obvious and stops the two drifting apart. A separate file collects every cost decision in one place, which sounds tidy and in practice makes each effect harder to reason about on its own.
Does this conflict with progressive enhancement?
No β it is the same principle applied to a second axis. Progressive enhancement asks what happens when a capability is missing; cost limiting asks what happens when the capability exists and the budget does not. Both start from a baseline that works and add from there.
How do I know the limit is set at the right point?
Measure on the device that motivated it. A cost breakpoint chosen from a design systemβs existing breakpoints is a guess; one chosen because a specific device improved measurably at that point is a finding. If no device improved, the limit is not needed.
Is it acceptable to show fewer effects on mobile?
Yes, and it is more honest than the alternative of showing all of them badly. The reader on a constrained device is not owed the same number of animations; they are owed a page that works, reads well and does not stutter. A section with three reveals instead of nine loses very little and gains a great deal.
What is not acceptable is a cost branch that removes content rather than motion. The test is simple: with every animation disabled, the page must still say everything it was going to say.
Should cost limits be tied to the design systemβs breakpoints?
Only if they happen to land in the right place. Design breakpoints exist to change layout, and the point at which a layout should reflow is not the point at which a device runs out of texture memory. Choosing a cost breakpoint because a specific device measurably improved there produces a number that means something; inheriting one from the layout produces a number that is easy to explain and unrelated to the problem.
Related
- Browser Support & Progressive Enhancement β capability guards in depth
- Safari and WebKit performance notes β the constraint most cost limits are written for
- Implementing prefers-reduced-motion β the preference that must win over both other conditions
- GPU Memory & Layer Budgets β quantifying the cost a limit is meant to control