
Motion that holds 60fps is an architecture problem long before it is an animation problem. The frame budget is 16ms, and almost every way a page loses it is decided during layout, not during the transition. By the time an engineer is tuning easing curves to chase a dropped frame, the real bug was usually committed weeks earlier, in a decision about where a style recalculation happens.
Compositing is the whole game
Transform and opacity are handled by the compositor, on its own thread, independent of the main thread's layout and paint work. Everything else — width, top, box-shadow, filter in some browsers — re-enters layout or paint, and once that happens no amount of easing tuning recovers the frame. The browser doesn't degrade gracefully here; it just drops frames, and a 60fps animation that occasionally triggers layout looks worse than a deliberately simpler 30fps one, because the stutter is inconsistent and reads as broken rather than stylized.
- Animate transform and opacity; treat anything else as a bug report.
- Promote sparingly — every layer costs memory on mobile, and a page with forty promoted layers will stutter on a mid-range phone even if each animation is individually compositor-only.
- Measure on a mid-range Android, never on the machine that built it — the gap between a developer's laptop and a user's three-year-old device is where most 'it felt fine in testing' regressions hide.
- will-change is a promise, not a suggestion — set it immediately before the animation starts and remove it immediately after, or the browser keeps paying the memory cost for a layer that's no longer moving.
Where WebGL and canvas actually earn their complexity
Most interfaces never need a shader. CSS transforms, handled right, cover card transitions, parallax, modal choreography, and the vast majority of what a product team calls 'motion.' WebGL earns its complexity when the visual is generative rather than a transition between two known states — particle fields, fluid gradients, anything where the frame is computed rather than interpolated between a start and end value a designer specified.
- Shader compilation stalls the first paint — compile and warm up off the critical path, not on the frame the user is waiting for.
- A WebGL context is a finite, shared browser resource; losing one unexpectedly (context loss) needs a real recovery path, not a blank canvas.
- Canvas work still competes with the main thread unless it's moved to an OffscreenCanvas on a worker — for anything running continuously, that move is not optional.
Building the pipeline that doesn't drop frames
Profile before touching a single easing curve
Open the performance panel and find out whether the dropped frame is layout, paint, or script before changing anything. Tuning duration and easing on a frame that's dropping because of forced layout is wasted effort.
Batch DOM reads and writes
Interleaving element.offsetHeight reads with style writes inside the same frame forces synchronous layout — a 'layout thrash' that silently costs milliseseconds per interaction. Read everything first, then write.
Budget paint area, not just JavaScript
A cheap script that triggers a full-viewport repaint loses to an expensive script that only repaints a 200px region. Constrain what actually has to redraw before optimizing the code that triggers it.
A note on reduced motion
Every animation we ship checks prefers-reduced-motion and has a real fallback, not just a shorter duration. For users who've set that preference at the OS level, parallax and auto-playing motion aren't an aesthetic choice they've opted out of — for some, it's the difference between using the product and closing the tab.
In closing
The fastest motion work is the work that never touches layout. Get the architecture right — compositor-only properties, a profiled pipeline, a real reduced-motion path — and the choreography becomes a styling decision rather than a performance negotiation.




