Command Palette

Search for a command to run...

Hectal
PHASE 10Advanced ~30 min· topic 5 of 6

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 reduce when 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.

Find out what a property costsdiagram
Rendering diagram…

02Move with translate, not left

Both rules slide a box 200px. The first redoes layout on every frame; the second only moves a layer.

style.csswhole filecss
/* 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.

style.csswhole filecss
.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.

style.csswhole filecss
.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.

style.csswhole filecss
.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. 1

    Block and watch

    In the first example, change the busy time to 2000 and click. Predict which box freezes and which keeps moving before you click. Then change the green box's keyframes to animate margin-left instead: now both freeze.

  2. 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. 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 the change listener.

Code & diagrams

Busy main thread: left freezes, transform keeps going New tab

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

Press Run to see the page.

Console

Nothing logged yet.
The Web Animations API: animate, finish, keep the result New tab

`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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

will-change: transform
playState: finished
transform: matrix(1, 0, 0, 1, 120, 0)
inline style: translateX(120px)
Respecting reduced motion New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

reduce motion: false
Layout thrashing and the fix (JavaScript)javascript
const 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.

terminal
$ the page shows
── what you'll see ──
Scrolling becomes slower, not faster; on phones the tab may reload or go blank. DevTools' Layers panel shows hundreds of layers and a large memory figure.

Break #2

Animating height for an accordion

Animate height (or max-height) of a large accordion panel with lots of content below it.

terminal
$ DevTools → Performance
── what you'll see ──
Every frame shows a long purple Layout block, and frames are dropped (red marks in the Frames track); the page stutters on a throttled CPU.

Break #3

Ignoring reduced motion

Build a parallax hero with large scrolling movement and no prefers-reduced-motion rule.

terminal
$ a user with reduced motion turned on
── what you'll see ──
They still see the full parallax motion, which can cause dizziness or nausea, and they may leave the site.

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/opacity animation, 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 from 1 up) keeps the final state sharp.

  • ▸

    content-visibility: auto lets the browser skip layout and paint for off-screen sections, and contain: layout paint limits how far a change can spread. Both reduce per-frame cost on long pages (pair content-visibility: auto with contain-intrinsic-size to 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. 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. 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, filter in many cases) skips layout but repaints. Changing **transform or opacity on 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. 3

    The compositor thread runs separately from the main thread (where your JavaScript, style and layout run). A CSS animation or transition of transform or opacity can be handed over to it entirely, so it keeps running smoothly even while the main thread is busy with a long script. An animation of left can'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 with translate and visibility with opacity. (Chromium can also composite some filter, clip-path and background-color animations, but transform and opacity are the reliable ones everywhere.)

  4. 4

    will-change: transform (or opacity) 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"), and will-change: transform also 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. 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, use requestAnimationFrame, which runs your code right before the next frame.

  6. 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

01

Why are transform and opacity the preferred properties to animate?

02

What is the compositor thread, and why does it keep some animations smooth while JavaScript is busy?

03

What does will-change do, and what are the risks of overusing it?

04

What is layout thrashing and how do you avoid it?

05

How should a site respond to prefers-reduced-motion: reduce?

Practice

01

Rewrite this animation so it doesn't trigger layout: @keyframes drop { from { top: -50px; } to { top: 0; } } on an absolutely positioned toast.

02

Make a .spinner that spins forever, but for reduced-motion users stops spinning and pulses its opacity slowly instead.

03

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 transform and opacity keeps 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 transform and visibility with opacity.

  • I use will-change sparingly 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.