Topic 10.5
Animation Performance and Accessibility
In one line
Smooth animation means producing a new frame every 16ms or so; animating transform and opacity lets the browser skip layout and paint and move ready-made layers on the compositor, will-change hints at this ahead of time, and prefers-reduced-motion lets you tone motion down for people it makes ill.
Think of it like this
A school play. Moving a cardboard cut-out across the stage is quick: one helper slides it. Rebuilding the whole set between scenes (moving walls, re-measuring where every prop goes, repainting the backdrop) takes ages, and the audience waits. Animating transform or opacity is sliding the cut-out. Animating width, top or margin is rebuilding the set on every single frame.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Frame
- One picture of the page on screen. Smooth motion needs about 60 or more per second.
- Jank
- A visible stutter when the browser misses frames because it has too much work.
- Main thread
- The browser's worker that runs your JavaScript, style calculation and layout, one task at a time.
- Compositor thread
- A separate worker that puts painted layers together on screen and can move or fade them on its own.
- Compositor layer
- A part of the page painted into its own picture, which the GPU can move, scale or fade cheaply.
- `will-change`
- A hint telling the browser which property is about to change, so it can prepare a layer in advance.
- Layout thrashing
- Code that writes styles and reads layout values in turn, forcing the browser to do layout again and again.
- Vestibular disorder
- A problem with the body's balance system, which can make on-screen motion cause dizziness or nausea.
- `prefers-reduced-motion`
- A media query that is
reducewhen the user has asked their device to show less motion.
Step by step
01Find out what a property costs
Before you animate a property, ask which steps of the pipeline it triggers. transform and opacity can skip straight to compositing. Colours and shadows repaint. Sizes and positions redo layout, which is the most expensive.
02Move with translate, not left
Both rules slide a box 200px. The first redoes layout on every frame; the second only moves a layer.
/* Expensive: layout every frame */
@keyframes slide-left { to { left: 200px; } }
/* Cheap: compositor only */
@keyframes slide-transform { to { transform: translateX(200px); } }03Fake expensive effects with cheap ones
Animating box-shadow repaints every frame. Instead, put the big shadow on a pseudo-element that is always there, and animate only its opacity.
.card { position: relative; }
.card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 12px 30px rgb(0 0 0 / 0.3);
opacity: 0;
transition: opacity 0.2s;
}
.card:hover::after { opacity: 1; }04Hint, but sparingly
Add will-change only to elements that really animate, for example when the user hovers the area that starts the animation, and let it go afterwards.
.menu-area:hover .drawer {
will-change: transform; /* prepare while the pointer is near */
}
.drawer.open {
transform: translateX(0);
}05Respect reduced motion
Write the motion for everyone, then tone it down for people who asked. Some teams prefer the reverse: no motion by default and motion inside @media (prefers-reduced-motion: no-preference), so browsers that don't report the setting get no motion.
.hero-image { animation: drift 20s linear infinite alternate; }
@media (prefers-reduced-motion: reduce) {
.hero-image { animation: none; }
.slide-in { animation-name: fade-in; } /* fade instead of fly in */
html { scroll-behavior: auto; } /* no smooth scrolling */
}06Measure, don't guess
Open DevTools' Performance panel, record while the animation runs, and look for long purple (Layout) and green (Paint) blocks in each frame. The Rendering drawer has Paint flashing (green flashes show repainted areas) and Layer borders, and the Layers panel shows every compositor layer and its memory. Throttle the CPU 4× to see what a cheap phone feels.
Try it yourself
- 1
Block and watch
In the first example, change the busy time to
2000and click. Predict which box freezes and which keeps moving before you click. Then change the green box's keyframes to animatemargin-leftinstead: now both freeze. - 2
Turn on paint flashing
Open DevTools → Rendering (from the
⋮menu → More tools) and tick Paint flashing. Watch the first example: the red box's lane flashes green on every frame (repaints), the green box doesn't. - 3
Emulate reduced motion
In DevTools → Rendering, set Emulate CSS media feature prefers-reduced-motion to
reduce. Predict what the third example's banner and status line do, then check. The page updates live because of thechangelistener.
Code & diagrams
Click the button. The red box (animating `left`, which needs layout on the main thread) freezes for a second. The green box (animating `transform`, handed to the compositor) keeps sliding. `100cqw` is the lane's width (a container query unit, Topic 9.6), because `translate` percentages refer to the box itself.
Preview
Console
Nothing logged yet.`el.animate()` uses the same engine as CSS animations, so a `transform` animation can run on the compositor too. `finished` is a promise, more reliable than events. `commitStyles()` plus `cancel()` keeps the end state without leaving a filling animation alive.
Preview
Console
Nothing logged yet.Expected console output
will-change: transform
playState: finished
transform: matrix(1, 0, 0, 1, 120, 0)
inline style: translateX(120px)The console shows `false` unless your device asks for reduced motion (then it prints `true` and the banner stays still). Turn it on to test: Windows: Settings → Accessibility → Visual effects → Animation effects off; macOS: Accessibility → Display → Reduce motion; or DevTools → Rendering → Emulate CSS prefers-reduced-motion. The `change` listener updates the page without a reload.
Preview
Console
Nothing logged yet.Expected console output
reduce motion: falseconst items = document.querySelectorAll(".item");
// Slow: each loop writes a style, then reads layout,
// so the browser must run layout once per item.
for (const el of items) {
el.style.width = "50%";
console.log(el.offsetHeight); // forced synchronous layout
}
// Fast: read everything first, then write everything.
const heights = [...items].map((el) => el.offsetHeight); // one layout
items.forEach((el, i) => {
el.style.height = heights[i] + 10 + "px"; // writes only
});
// For JavaScript-driven motion, change styles once per frame:
function step(time) {
box.style.transform = `translateX(${(time / 10) % 300}px)`;
requestAnimationFrame(step);
}
requestAnimationFrame(step);Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
will-change on everything
Add * { will-change: transform; } to a long page with hundreds of elements.
Break #2
Animating height for an accordion
Animate height (or max-height) of a large accordion panel with lots of content below it.
Break #3
Ignoring reduced motion
Build a parallax hero with large scrolling movement and no prefers-reduced-motion rule.
Myth vs fact
Myth
Adding transform: translateZ(0) or will-change makes everything faster.
Fact
Extra layers cost memory and compositing time, and change stacking and positioning. They only help elements that really animate, and only if measuring shows a benefit.
Myth
Reduced motion means no animation at all.
Fact
It means less movement, not zero change. Fades, colour changes and small feedback are usually fine; large movement, zooming, parallax and auto-playing loops should go.
Myth
CSS animations are always smoother than JavaScript animations.
Fact
What matters is the property and the thread. A CSS animation of width janks; a Web Animations API animation of transform can run on the compositor just like CSS. requestAnimationFrame loops always run on the main thread.
Myth
If it looks smooth on my laptop, it's fine.
Fact
A developer laptop is several times faster than a mid-range phone. Test with CPU throttling in DevTools and on a real device.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Elements get compositor layers for many reasons: a running
transform/opacityanimation,will-change, 3D transforms,<video>,<canvas>, fixed elements, and overlap (an element painted above a layer may need its own layer, called layer squashing or implicit compositing). DevTools' Layers panel shows each layer's "compositing reasons". - ▸
A scale animation on a compositor layer scales the already painted texture, so text can look blurry mid-animation; the browser re-rasterises at the end. Animating from a smaller scale up to
1(rather than from1up) keeps the final state sharp. - ▸
content-visibility: autolets the browser skip layout and paint for off-screen sections, andcontain: layout paintlimits how far a change can spread. Both reduce per-frame cost on long pages (paircontent-visibility: autowithcontain-intrinsic-sizeto avoid scrollbar jumps). - ▸
Interaction to Next Paint (INP), a Core Web Vital, measures how long the page takes to show a frame after a click or key press. Long main-thread tasks hurt it; animations that stay on the compositor don't. The "three flashes" rule comes from WCAG 2.3.1, and "pause, stop, hide" from WCAG 2.2.2.
Remember this
- 1
A screen usually refreshes 60 times per second (many phones and monitors do 90, 120 or more). For motion to look smooth, the browser must deliver a new frame each time, which leaves about 16.7ms per frame at 60Hz, and less at higher rates. When a frame takes longer, the browser skips it, and you see a stutter called jank. Each frame, the browser may have to run JavaScript, recalculate style, redo layout (sizes and positions), paint (draw pixels into layers) and composite (put the layers together on screen). The less of that pipeline a change needs, the cheaper it is.
- 2
What each kind of property costs: changing a layout property (
width,height,top,left,margin,padding,font-size,border-width) forces layout, then paint, then composite, and layout can spread to the parent and siblings. Changing a paint-only property (color,background-color,box-shadow,border-radius,filterin many cases) skips layout but repaints. Changing **transformoropacityon an element that has its own compositor layer (a separately painted picture the GPU can move) needs neither: the compositor thread** moves or fades the layer it already has. - 3
The compositor thread runs separately from the main thread (where your JavaScript, style and layout run). A CSS animation or transition of
transformoropacitycan be handed over to it entirely, so it keeps running smoothly even while the main thread is busy with a long script. An animation ofleftcan't: if a script blocks the main thread for a second, the box freezes for a second. That's the strongest reason to animate position withtranslateand visibility withopacity. (Chromium can also composite somefilter,clip-pathandbackground-coloranimations, buttransformandopacityare the reliable ones everywhere.) - 4
will-change: transform(oropacity) is a hint: "this property is about to animate, prepare now." The browser may give the element its own compositor layer ahead of time, so the first frame of the animation doesn't stutter. It has costs: every layer uses GPU memory, too many layers slow things down ("layer explosion"), andwill-change: transformalso creates a stacking context and a containing block for fixed descendants, just like a real transform. Use it on a few elements that really animate, ideally added shortly before the animation and removed after, never as* { will-change: transform; }. - 5
Layout thrashing is a JavaScript-made performance bug: reading a layout value (
offsetHeight,getBoundingClientRect()) right after writing a style forces the browser to do layout immediately (a forced synchronous layout). Doing write, read, write, read in a loop causes a layout per item. Batch your reads first, then your writes, or animate with CSS or the Web Animations API (el.animate()), which lets the browser schedule the work. For JavaScript-driven motion, userequestAnimationFrame, which runs your code right before the next frame. - 6
Motion can make some people ill: people with vestibular disorders (problems with the inner-ear balance system) can get dizziness, nausea or headaches from parallax, zooming and big sliding animations. Operating systems have a "reduce motion" setting, and CSS reads it with the media query
@media (prefers-reduced-motion: reduce)(Topic 9.3). Respect it: replace large movement with a fade or an instant change, stop auto-playing and infinite animations, and keep only small, meaningful feedback. Separately, WCAG (the web accessibility guidelines) requires that anything that moves automatically for more than 5 seconds can be paused, and that nothing flashes more than three times per second, which can trigger seizures.
Explain it without notes
Why are transform and opacity the preferred properties to animate?
What is the compositor thread, and why does it keep some animations smooth while JavaScript is busy?
What does will-change do, and what are the risks of overusing it?
What is layout thrashing and how do you avoid it?
How should a site respond to prefers-reduced-motion: reduce?
Practice
Rewrite this animation so it doesn't trigger layout: @keyframes drop { from { top: -50px; } to { top: 0; } } on an absolutely positioned toast.
Make a .spinner that spins forever, but for reduced-motion users stops spinning and pulses its opacity slowly instead.
Write JavaScript that slides a .panel 200px to the right with the Web Animations API (0.3s), but makes the change instantly when the user prefers reduced motion. Log "moved" when it's done.
Trade-offs
- ↔
Restricting yourself to
transformandopacitykeeps motion smooth but limits what you can animate; a short layout animation on a small, contained element is often fine. Measure before optimising. - ↔
Compositor layers make animations cheap but cost memory; on low-end phones, many large layers can be slower than repainting.
- ↔
Reduced-motion versions need design and testing time, but they make the site usable for people who would otherwise be made ill by it.
Done when you can
I know which properties trigger layout, paint or only composite.
I animate movement with
transformand visibility withopacity.I use
will-changesparingly and know its side effects.I can spot and fix layout thrashing.
I can use the Performance panel, paint flashing and the Layers panel.
I provide a reduced-motion version and never flash more than three times a second.