Chapter 06 of 11Part 2 · CSS
Transform, opacity and compositor only animation
Why transform and opacity animate smoothly when other properties stutter, and how animation affects responsiveness and loading.
10 minBy Zuzo StudioUpdated
In this chapter you will
- explain what layout, paint and composite each do to draw a frame
- rewrite an animation so it changes only transform and opacity
- audit your hero so nothing stutters and the heading is visible on first paint
Before you startThis builds on CSS keyframe animation: @keyframes and the animation property.
Two animations can look identical in a design file and behave completely differently on a phone. One glides, the other stutters the moment the page has anything else to do. The difference is usually not the easing or the duration but the property being animated. Compositor only animation means changing just transform and opacity, the two properties the browser can update without working out the layout of the page or repainting any pixels. This chapter explains what the browser does to draw each frame, why those two properties are cheap, how animation affects responsiveness, and when will-change helps. Then you audit your hero against all of it.
01What the browser does to draw a frame
Every visual change on a page goes through a short chain of work before it reaches the screen. web.dev's guide to rendering performance calls it the pixel pipeline and names five stages:
- JavaScript. A script changes something: adds a class, sets a style, starts an animation.
- Style. The browser works out which CSS rules now apply to which elements.
- Layout. It calculates where every box sits and how big it is. A change to one element can move many others.
- Paint. It fills in the pixels: text, colours, borders, shadows, images. Pages are painted in several layers.
- Composite. It puts those layers together, in the right order and position, to make the final image.
An animation repeats this chain for every frame. The same guide works out the budget: a typical screen refreshes 60 times a second, which gives the browser 16.66 milliseconds per frame, and because the browser has its own housekeeping, your work needs to fit in about 10. Miss the budget and the frame is dropped. Drop a few in a row and the eye sees a stutter, which developers call jank.
The trick is that not every change needs every stage. If you change a box's width, the browser has to run layout, then paint, then composite. If you change its colour, it can skip layout but still has to paint. If you change only its transform or opacity, it can skip both and go straight to compositing, moving or fading a layer it has already painted.
02Which properties cost what
As a working rule, sort every animated property into one of three groups.
| Triggers | Examples | Cost per frame |
|---|---|---|
| Layout, paint and composite | width, height, top, left, margin, padding, font-size |
Highest: other boxes may move too |
| Paint and composite | color, background-color, box-shadow, border-radius, filter on large areas |
Medium: pixels are redrawn |
| Composite only | transform, opacity |
Lowest: an existing layer is moved or faded |
web.dev's article on compositor only properties sums it up in one line: stick to transform and opacity changes for your animations. Almost every common movement can be rewritten that way. Move with translate() instead of left, grow with scale() instead of width, and fade with opacity instead of toggling visibility. Our website animation performance article has the full table of swaps, including the awkward ones like shadows and panels that open to their content height.
Colour changes on small elements, such as a button background on hover, are paint work but so small that they are rarely a problem. The rule matters most for things that move, things that are large, and anything that runs for a long time or many times at once.
03Compositor only animation, side by side
Most of the pipeline runs on the main thread: the same single line of work that also runs your JavaScript, handles clicks and parses HTML. When the main thread is busy, everything queued on it waits, including the next frame of an animation that needs layout or paint.
Compositing is different. Browsers can hand composite only work to a separate compositor thread, and the same web.dev article warns that non-composited animations can appear janky on low end phones or when heavy tasks run on the main thread. So a CSS animation or transition on transform or opacity can carry on smoothly even while a script is busy.
The demo below moves two identical boxes back and forth. The top one is animated with left, the bottom one with transform: translateX(). Press "Start both", then "Make the main thread busy": the demo burns CPU on every frame, the way a heavy script or a slow page would, and the readout shows how few frames a second the main thread now manages. Watch the left box start to stutter while the transform box keeps its pace, because the compositor can move it without waiting for the main thread.
Moving a box by changing its left position makes the browser redo layout on every frame. Moving it with transform lets the compositor move it without layout or paint, which stays smooth on slower devices.
On a fast laptop the gap may look small until you switch the busy toggle on. On a mid range phone it shows up without any help. That is why we test on real devices, a habit the website speed chapter of the first course sets up.
04The main thread, INP and animation
Animation affects more than smoothness. It competes with the reader for the main thread. Interaction to Next Paint, or INP, measures how long a page takes to respond visually after a tap, click or key press. It became a Core Web Vital on 12 March 2024, replacing First Input Delay. web.dev's INP guide sets the thresholds: 200 milliseconds or less is good, and more than 500 is poor.
Three animation habits push INP up:
- Layout in a loop. Animating
height,topormarginwith JavaScript runs layout on every frame, and an interaction that arrives mid frame waits for it. - Reading and writing in turn. Reading a size such as
offsetHeightright after changing a style forces the browser to run layout early. Doing it for many elements in one frame, often called layout thrashing, can take far longer than the frame budget. - Scroll handlers that do the work. Code attached to every scroll event runs constantly while the reader scrolls. Chapter 9 covers the tools that avoid it.
A tap that starts an animation should also feel answered straight away. Put the visible response, a pressed state or the first frame of the movement, on the very next frame, and leave any expensive setup until after it.
05will-change, sparingly
will-change tells the browser that a property is about to change, so it can prepare, usually by giving the element its own compositor layer ahead of time. That can remove a stutter on the first frame of a big animation. It also has a cost. web.dev points out that every layer needs memory and management, and that on devices with limited memory the cost can outweigh the benefit.
MDN's will-change reference is blunt about it: use it as a last resort to deal with existing performance problems, not to anticipate them. In practice:
/* Only while it matters */
.menu.is-opening { will-change: transform; }
/* Never */
* { will-change: transform; }
Add it to the specific element just before a known heavy animation, through a class or from your script, and remove it once the animation ends. If you have not measured a problem, leave it out.
06The first screen: never wait on motion
The cheapest animation in the world still hurts if it hides the content people came for. Largest Contentful Paint, the Core Web Vital for loading, measures when the biggest piece of content on the first screen is painted. In most heroes that is the heading. A heading that starts at opacity: 0 is not counted as painted while it is invisible, so every moment it waits before it fades in is added to the reader's wait, and to the metric.
So the hero heading and buttons should be readable in the HTML and CSS from the first paint. Motion can move them, from slightly lower or slightly smaller, but never from invisible. Decorative pieces such as your scroll cue can start later, because nobody is waiting for them. The Core Web Vitals checklist explains how to measure all three vitals on a real page.
Check your understanding
3 questions
Key takeaways
- Each frame can need style, layout, paint and composite work, and the browser has about 10 milliseconds of it per frame at 60 frames a second.
- Animating transform and opacity skips layout and paint, so the browser only composites layers it has already drawn.
- Composited CSS animations can keep running smoothly while the main thread is busy; layout animations cannot.
- Use will-change as a last resort for a measured problem, on one element, and remove it afterwards.
- Motion can move the hero heading but must never start it invisible, because that delays Largest Contentful Paint.
Sources
- Rendering performanceweb.dev, checked
- Stick to compositor-only properties and manage layer countweb.dev, checked
- Interaction to Next Paint is officially a Core Web Vitalweb.dev, checked
- Interaction to Next Paint (INP)web.dev, checked
- will-changeMDN Web Docs, checked