Animation Performance: Engine-by-Engine Support Matrix
Support tables usually answer “does this work”. For animation performance the more useful question is “does this work the same way” — and there the answer is more nuanced, because the specification fixes the semantics and leaves the compositing heuristics, memory limits and input curves entirely to the implementation. This topic collects what is genuinely consistent across engines, what is not, and how to verify the difference without profiling everything three times. It sits under animation performance, profiling and optimization.
Guides in this topic
- Chromium compositor behaviour for scroll timelines — layer promotion, tiling, and what the Layers panel is telling you.
- Safari and WebKit scroll animation performance notes — memory pressure, texture eviction and iOS-specific limits.
- Firefox and Gecko scroll-driven animation support — what the profiler shows and what it cannot.
- Measuring frame rate across engines — one measurement that works everywhere.
- @supports guard recipes for performance-sensitive effects — guarding on cost rather than on capability.
Feature inventory and syntax reference
Everything in this topic assumes the same small surface, guarded the same way:
/* the capability query — support for the API */
@supports (animation-timeline: scroll()) { /* … */ }
@supports (view-transition-name: none) { /* … */ }
/* the cost controls — how much the browser is allowed to spend */
.card {
will-change: transform; /* reserve a layer, briefly */
contain: paint; /* bound what a repaint can touch */
content-visibility: auto; /* skip rendering off-screen sections */
}
The first group is portable and behaves identically everywhere. The second group is where engines diverge: all three honour the properties, and none of them promises the same heuristics behind them.
What is specified and what is not
This distinction is the single most useful thing to hold onto when a page behaves differently in two browsers. If the difference is in what happened — an animation ran, a transition paired, a range applied — it is a specification question and one of the engines has a bug worth reporting. If the difference is in how it felt — smoothness, memory, momentum — it is an implementation question, and the fix is almost always to reduce cost rather than to branch on the engine.
Minimal working example
A section that measures its own frame timing works in every engine and needs no devtools:
let last = performance.now();
let worst = 0;
function frame(now) {
const delta = now - last;
if (delta > worst) worst = delta;
last = now;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// after scrolling through the section:
console.log('worst frame:', worst.toFixed(1), 'ms');
A worst-frame figure above about 32 milliseconds means at least one frame was dropped at 60 Hz. This is a blunt instrument and it is the only measurement available identically in all three engines, which makes it the right first comparison.
Compositor-safe properties across engines
The compositor-safe set — transform, opacity, filter, and interpolable clip-path — is consistent. What differs is what each engine does when you leave that set, and how aggressively it promotes layers when you stay inside it.
Chromium promotes generously and exposes the result in a Layers panel, which makes it the easiest engine to over-promote in and the easiest to diagnose. Safari promotes similarly but operates under much tighter memory ceilings on iOS, where exceeding them causes texture eviction rather than slow frames. Firefox promotes more conservatively and offers no layer viewer, so over-promotion there is inferred from paint behaviour rather than observed directly.
The practical consequence is that a page tuned only in Chromium can be well inside its own budget and still fail on iOS — not because the animation is different but because the ceiling is.
Common implementation patterns
Pattern 1 — profile once, verify everywhere. Do the detailed work in the engine with the best tooling, then verify the conclusion in the others with a frame-timing measurement rather than a second full profile.
Pattern 2 — budget for the tightest ceiling. Set the layer and texture budget from the most constrained target device rather than from the development machine, and check it there rather than extrapolating.
Pattern 3 — guard on cost, not only on capability. A feature query answers whether an effect can run. On a page with many effects, whether it should is a separate decision, and content-visibility plus a reduced effect set for small viewports is the usual expression of it.
Pattern 4 — treat momentum differences as a design constraint. Because momentum curves differ, an effect that depends on a particular scroll velocity to look right will not look the same everywhere. Effects that read well at any velocity are the ones that travel.
Browser support and @supports guard
There is no feature query for performance. The queries available test capability, and the guard patterns that matter here are about limiting how much work is requested rather than about whether the API exists:
@media (prefers-reduced-motion: reduce) {
.card { animation: none; will-change: auto; }
}
/* fewer simultaneous effects where memory is tightest */
@media (max-width: 40rem) {
.stack .card:nth-child(n + 4) { animation: none; }
}
Gotchas and failure modes
-
Assuming Chromium’s behaviour is the specification. Layer promotion heuristics are not specified. A page that depends on a particular promotion happening automatically is depending on an implementation detail.
-
Profiling only on the development machine. Memory ceilings, not CPU time, are what break these pages, and the development machine has the loosest ceiling in the fleet.
-
Branching on the engine. Engine sniffing to work around a performance difference locks in behaviour that changes with the next release. Reduce the cost instead; every engine benefits.
-
Treating the absence of a layer viewer as the absence of layers. Firefox promotes layers; it simply does not show them. Inferring “no layers” from “no panel” leads to over-promotion that nobody notices.
-
Comparing recordings taken with different throttling. Cross-engine comparisons are only meaningful when the CPU and network conditions match, and the throttling controls differ between the three sets of devtools.
Performance checklist
- Profile in depth in one engine; verify with frame timing in the others.
- Set the layer budget from the most constrained device you support.
- Re-check after any dependency upgrade that touches animation.
- Keep a worst-frame measurement in the test suite so regressions surface without a manual profile.
- Never branch on engine; reduce cost instead.
Reading a support table for performance rather than capability
Compatibility data answers whether a feature is implemented. It does not answer any of the questions that decide whether a page feels good, and treating it as though it does is how teams end up surprised by a device class that every table said was supported.
Support does not imply parity of cost. Every current engine runs a transform animation on the compositor. That tells you the animation will not block the main thread; it tells you nothing about whether the layer it needs will fit alongside the twelve others the page has already promoted. The capability is identical and the outcome is not, and no support table records the second part.
Support does not imply parity of tooling. All three engines promote layers. Only two of them let you look at the result, and only two have an animation inspector. A verification workflow built on a panel that exists in one engine cannot be repeated in the others, which is why the routine in this topic is built around a measurement rather than an inspection.
Support arrives before optimisation does. A feature that has just shipped in an engine is usually correct and not yet fast. That is normal and temporary, and it means a performance conclusion drawn in the first months after a feature ships has a short shelf life. Re-measuring after a major browser release is worth the ten minutes, particularly for anything that was marginal.
Flags are not a support signal. A capability behind a flag tells you the implementation exists and nothing about how it performs when it ships, because performance work usually happens between the flag and the default. Testing behind a flag is useful for confirming your enhancement path works; it is not useful for predicting cost.
The practical translation of all four is that a support table belongs at the start of a decision — can we use this at all — and a measurement belongs at the end of it. Substituting one for the other in either direction is where the surprises come from.
Building a device matrix that is worth maintaining
A cross-engine test plan that lists every browser and every operating system is unmaintainable and, worse, unread. A useful matrix is small, chosen for what each row can uniquely reveal, and stable enough that results are comparable over time.
Four rows cover most sites. A current Chromium build on a development machine is the profiling environment, chosen for tooling rather than for representativeness. A current Safari on macOS covers WebKit’s compositing behaviour without the mobile memory ceiling. A current Firefox covers Gecko, which is the engine most likely to reveal an assumption baked in from Chromium-only development. And one mid-range Android phone — genuinely mid-range, not a flagship two years old — covers the memory ceiling that decides whether a media-heavy page holds together.
An iOS device is a fifth row worth adding for any site with heavy imagery, because iOS Safari’s memory behaviour under pressure is unlike anything the other four will show you. It is also the row most likely to be skipped, because the failure mode there — a blank frame where a layer should be — is easy to dismiss as a glitch when you see it once.
What makes such a matrix maintainable is keeping the measurement identical across rows. A single worst-frame figure and a layer count, recorded the same way each time, produce a table that can be compared release over release. A collection of screenshots and impressions does not, and it is the reason most cross-browser performance testing quietly stops happening after the first month.
Frequently Asked Questions
Should I test in every engine before shipping every change?
No, and trying to is why cross-engine testing gets abandoned. Test in every engine when the change touches the animation surface — a new effect, a new promoted element, a dependency upgrade that changes how something animates. Routine content and layout changes do not need it, because the animation behaviour they inherit was already verified.
What if an effect is smooth in two engines and not the third?
Reduce the cost rather than branching. In practice a divergence of that shape almost always means the effect was close to a limit in all three and only crossed it in one — fewer promoted layers, smaller textures or a shorter effect will usually bring all three comfortably inside. Engine-specific workarounds fix the symptom and expire with the next release.
Is Chromium’s Layers panel trustworthy for other engines?
For the count and the reasons, no — those are Chromium’s own heuristics. For the principle, yes: an element that Chromium promotes for a good reason is usually promoted for the same reason elsewhere, and an element Chromium promotes because of a stray will-change is wasting memory in every engine. Use it to find unnecessary promotions, not to predict another engine’s layer tree.
How much does device pixel ratio actually matter?
More than almost anything else on this page. Texture memory scales with the square of the ratio, so a layer that costs one megabyte on a standard display costs about nine on a three-times device. A page that promotes generously and is only ever tested at 1x is testing the least demanding configuration it will ever encounter.
Does the same advice apply to view transitions?
Largely, with one addition. The transition lifecycle is specified, so the sequence of events is identical everywhere and a timing bug in one engine is a genuine bug. What is not specified is the snapshot memory each engine reserves and how it behaves when that reservation cannot be met — which is the same memory-ceiling story as layer promotion, arriving through a different door.
The practical consequence is that a transition with a handful of named elements is portable, and one with twenty is a memory experiment whose result differs per device. Keeping the named count low is the portability measure, not a per-engine workaround.
How do I keep a performance budget from going stale?
Attach it to a measurement that runs automatically rather than to a document. A worst-frame figure and a layer count captured by the same browser automation that already runs the accessibility assertions will notice a regression the week it lands; a written budget reviewed quarterly will notice it after the release.
The second half is re-baselining deliberately. Browser releases change compositing behaviour, sometimes for the better, and a budget set against an old baseline eventually fails for reasons that have nothing to do with your code. Re-measure the baseline on a schedule and record when you did, so a failing check can be read as either a regression or a stale threshold rather than being ambiguous.
Which engine should a new effect be developed against?
Whichever one you will verify in last. Developing against the engine with the loosest constraints and checking the tightest at the end is how effects that need reworking get discovered after they have been signed off. Building the first version against a mid-range phone, where the memory ceiling is real, produces an effect that is comfortably inside budget everywhere else — and the desktop version costs nothing extra to add afterwards.
Related
- Profiling Scroll Animations in DevTools — the detailed profiling workflow this topic assumes
- Compositor-Safe Properties & will-change — the promotion mechanics that differ between engines
- GPU Memory & Layer Budgets — the budget this topic tells you to set per device class
- Browser Support & Progressive Enhancement — capability guards, as distinct from cost guards