Why the same animation is smooth in one property and jerky in another
Animate transform: translateX(300px) and it runs at the display’s full rate on a five-year-old phone. Animate left: 300px and the same movement stutters. The distance is identical, the duration is identical, and the difference is entirely about which stages of the rendering pipeline each one forces the browser to repeat.
The pipeline
Every frame, a browser can do up to five things:
- Style: work out which CSS rules apply and what the computed values are.
- Layout: work out where every box goes and how big it is. Changing one element’s geometry can move everything after it, so this is global work.
- Paint: fill in pixels: text, colours, borders, shadows, into one or more layers.
- Composite: take the painted layers and combine them into the final image, applying each layer’s transform and opacity.
- Present: hand it to the display.
The frame budget is the display’s refresh interval: 16.7ms at 60Hz, 8.3ms at 120Hz. Everything above must finish inside that, and the browser has other work to do in it too: roughly 10ms is a realistic budget at 60Hz.
Layout is the expensive stage and it is not local. Changing left on one absolutely positioned element is cheap; changing width on an element in normal flow can force the browser to re-measure hundreds of siblings. Paint is expensive in proportion to area and to how complex the content is: text and shadows are dear.
Transform and opacity are different. They are properties of a composited layer, not of its contents. Changing them does not change what was painted, only how the already-painted layer is placed and blended. So the browser can skip stages 1 to 3 entirely and, critically, can do the work on the compositor thread, which keeps running even when the main thread is blocked by JavaScript.
That last point is the one that matters on real devices. A transform animation survives a 200ms script task. A left animation freezes.
The practical list
Animate freely: transform (translate, scale, rotate, skew), opacity, and filter, though filters cost GPU time and can be slow on weak hardware.
Avoid animating: width, height, top, left, right, bottom, margin, padding, border-width, font-size: all trigger layout.
Avoid animating: box-shadow, background-position, border-radius, color: no layout, but a repaint every frame. Sometimes acceptable for a small element; noticeably expensive on a large one. A moving box-shadow on a full-width card is a common cause of a slow-feeling page, and the fix is to put the shadow on a pseudo-element and animate its opacity instead.
Layers are not free
will-change: transform promotes an element to its own compositor layer ahead of time, so the browser does not have to promote it mid-animation and drop a frame doing it.
It is also a memory allocation. A full-screen layer at 1920×1080 with 4 bytes per pixel is about 8MB of GPU memory, and on a low-end phone with a handful of such layers you will hit memory pressure and get worse performance, or crashes. The rules that follow from the mechanism:
- Add
will-changeshortly before the animation and remove it after. - Never put it on a long list of elements “just in case”.
- If you find yourself adding it everywhere, the problem is elsewhere.
Seeing it rather than guessing
Open DevTools → the three-dot menu → More tools → Rendering, and turn on Paint flashing. Any region that repaints flashes green. Run your animation. If large areas flash, you are painting every frame. This takes ten seconds and settles most arguments.
Then record in the Performance panel and look at the flame chart. Purple bands are layout and paint; a well-built transform animation shows almost none of them during the animation, just compositor work. The panel also marks long tasks over 50ms, which are what break main-thread animations.
The one exception worth knowing
The Web Animations API with element.animate() can hand a transform or opacity animation straight to the compositor, exactly like CSS. A requestAnimationFrame loop that sets style.transform each frame cannot: it is main-thread by definition, because your JavaScript has to run to produce each value.
This is the real reason to prefer declarative animation over a manual rAF loop: not elegance, but which thread the work lands on. When you need per-frame JavaScript (scroll-linked effects, physics) modern CSS scroll-driven animations and ScrollTimeline move a large part of that case back onto the compositor.
The free path
Chrome DevTools, Firefox Profiler and Safari’s Web Inspector all do everything described here, free. There is no paid tool that tells you more about a frame budget than Paint flashing and a performance recording.
The limitation
“Animate transform and opacity” is good advice that has hardened into a superstition. Plenty of real interfaces need a height to animate (an accordion opening is the obvious one) and the answer is not to refuse. It is to know the cost, keep the animating subtree small, and consider the alternatives: a scaleY with a counter-scaled child, a clip-path, or CSS grid-template-rows: 0fr → 1fr, which does trigger layout but on a much smaller subtree than the whole page.
The other limitation is that the compositor is GPU work, and on some low-end Android devices the GPU is the bottleneck rather than the CPU. Transform-only animation does not help there, and the only fix is fewer and smaller layers.
Every lesson here is open. Your progress is recorded once you pass the Video with AI exam.
Check your understanding
During a transform animation, a 200ms JavaScript task blocks the main thread. The animation continues smoothly. During an identical animation on the width property, it freezes. Why?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Deepa, your AI tutor