{"course":{"slug":"motion-and-audio","title":"Motion, Sound and Music","url":"https://addaly.com/learn/motion-and-audio","level":"intermediate","track":"create"},"lesson":{"slug":"compositor-only-properties","title":"Why transform is cheap and width is not","url":"https://addaly.com/learn/motion-and-audio/compositor-only-properties","position":18,"of":72,"minutes":10,"answer":"Transform and opacity describe how an already-painted layer is placed, so animating them skips layout and paint and can run on the compositor thread even while the main thread is blocked.","keyIdea":"Transform and opacity describe how an already-painted layer is placed, so animating them skips layout and paint and can run on the compositor thread even while the main thread is blocked, which is why they survive a heavy script task and a width animation does not."},"body":{"format":"markdown","text":"## Why the same animation is smooth in one property and jerky in another\n\nAnimate `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.\n\n## The pipeline\n\nEvery frame, a browser can do up to five things:\n\n1. **Style**: work out which CSS rules apply and what the computed values are.\n2. **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.\n3. **Paint**: fill in pixels: text, colours, borders, shadows, into one or more layers.\n4. **Composite**: take the painted layers and combine them into the final image, applying each layer's transform and opacity.\n5. **Present**: hand it to the display.\n\nThe 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.\n\n**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.\n\n> Figure: The five stages a browser can do in one frame. Layers, top to bottom: 1 Style (which rules apply, and what the values are); 2 Layout (where every box goes, not local work); 3 Paint (fill in pixels; costly by area); 4 Composite (place and blend the painted layers); 5 Present (hand the result to the display). The budget is 16.7 ms at 60 Hz, and the browser has other work inside it. Changing width re-runs stage two and everything after it, on the main thread. Transform and opacity are properties of an already-painted layer, so the browser skips stages one to three and can do the rest on the compositor thread, which keeps running while a 200-millisecond script task blocks the main one.\n\n**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.\n\nThat last point is the one that matters on real devices. A transform animation survives a 200ms script task. A `left` animation freezes.\n\n## The practical list\n\nAnimate freely: `transform` (translate, scale, rotate, skew), `opacity`, and `filter`, though filters cost GPU time and can be slow on weak hardware.\n\nAvoid animating: `width`, `height`, `top`, `left`, `right`, `bottom`, `margin`, `padding`, `border-width`, `font-size`: all trigger layout.\n\nAvoid 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.\n\n## Layers are not free\n\n`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.\n\nIt 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:\n\n- Add `will-change` shortly before the animation and remove it after.\n- Never put it on a long list of elements \"just in case\".\n- If you find yourself adding it everywhere, the problem is elsewhere.\n\n## Seeing it rather than guessing\n\nOpen 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.\n\nThen 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.\n\n## The one exception worth knowing\n\nThe 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.\n\nThis 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.\n\n## The free path\n\nChrome 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.\n\n## The limitation\n\n\"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.\n\nThe 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."},"check":{"question":"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?","options":[{"text":"Width animations are interpolated in JavaScript by the browser's animation engine, while transforms are interpolated in CSS, and only the CSS path is exempt from main-thread blocking.","correct":false,"tempting":"Attributes the difference to where the animation was declared rather than to which pipeline stages the property forces."},{"text":"A width animation needs layout recalculated every frame, and layout runs on the main thread; a transform only changes how an existing layer is composited, which the compositor thread can do alone.","correct":true},{"text":"The transform animation is being run by the GPU, which caches the whole animation in advance, so it would continue even if the page were closed mid-flight.","correct":false,"tempting":"Overstates compositor independence into a claim that the animation is precomputed and detached from the document."},{"text":"The width animation has triggered a repaint of the element's text, and text rasterisation is the only operation in the rendering pipeline that is pinned to the main thread while every other stage can be parallelised onto worker threads.","correct":false,"tempting":"Singles out text rasterisation; layout itself is the main-thread bottleneck, and paint is not the distinguishing factor here."}]},"prev":{"slug":"prefers-reduced-motion","title":"Motion that makes people ill","url":"https://addaly.com/learn/motion-and-audio/prefers-reduced-motion"},"next":{"slug":"which-technology","title":"Five ways to put motion on a screen","url":"https://addaly.com/learn/motion-and-audio/which-technology"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Why transform is cheap and width is not', in Motion, Sound and Music. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/motion-and-audio/compositor-only-properties (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Why transform is cheap and width is not","courseTitle":"Motion, Sound and Music","module":{"index":2,"slug":"motion-that-carries-meaning","title":"Motion that carries meaning"},"answer":null,"keyIdea":"Transform and opacity describe how an already-painted layer is placed, so animating them skips layout and paint and can run on the compositor thread even while the main thread is blocked, which is why they survive a heavy script task and a width animation does not.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"stack","title":"The five stages a browser can do in one frame","layers":[{"label":"1 Style","detail":"which rules apply, and what the values are"},{"label":"2 Layout","detail":"where every box goes, not local work"},{"label":"3 Paint","detail":"fill in pixels; costly by area"},{"label":"4 Composite","detail":"place and blend the painted layers"},{"label":"5 Present","detail":"hand the result to the display"}],"caption":"The budget is 16.7 ms at 60 Hz, and the browser has other work inside it. Changing width re-runs stage two and everything after it, on the main thread. Transform and opacity are properties of an already-painted layer, so the browser skips stages one to three and can do the rest on the compositor thread, which keeps running while a 200-millisecond script task blocks the main one."},"alt":"Figure: The five stages a browser can do in one frame. Layers, top to bottom: 1 Style (which rules apply, and what the values are); 2 Layout (where every box goes, not local work); 3 Paint (fill in pixels; costly by area); 4 Composite (place and blend the painted layers); 5 Present (hand the result to the display). The budget is 16.7 ms at 60 Hz, and the browser has other work inside it. Changing width re-runs stage two and everything after it, on the main thread. Transform and opacity are properties of an already-painted layer, so the browser skips stages one to three and can do the rest on the compositor thread, which keeps running while a 200-millisecond script task blocks the main one."}},"blocks":[{"t":"h2","id":"why-the-same-animation-is-smooth-in-one-property-and-jerky-i","runs":[{"text":"Why the same animation is smooth in one property and jerky in another"}]},{"t":"p","runs":[{"text":"Animate "},{"text":"transform: translateX(300px)","code":true},{"text":" and it runs at the display’s full rate on a five-year-old phone. Animate "},{"text":"left: 300px","code":true},{"text":" 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."}]},{"t":"h2","id":"the-pipeline","runs":[{"text":"The pipeline"}]},{"t":"p","runs":[{"text":"Every frame, a browser can do up to five things:"}]},{"t":"ol","start":1,"items":[[{"text":"Style","b":true},{"text":": work out which CSS rules apply and what the computed values are."}],[{"text":"Layout","b":true},{"text":": 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."}],[{"text":"Paint","b":true},{"text":": fill in pixels: text, colours, borders, shadows, into one or more layers."}],[{"text":"Composite","b":true},{"text":": take the painted layers and combine them into the final image, applying each layer’s transform and opacity."}],[{"text":"Present","b":true},{"text":": hand it to the display."}]]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"Layout is the expensive stage and it is not local.","b":true},{"text":" Changing "},{"text":"left","code":true},{"text":" on one absolutely positioned element is cheap; changing "},{"text":"width","code":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"Transform and opacity are different.","b":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"That last point is the one that matters on real devices. A transform animation survives a 200ms script task. A "},{"text":"left","code":true},{"text":" animation freezes."}]},{"t":"h2","id":"the-practical-list","runs":[{"text":"The practical list"}]},{"t":"p","runs":[{"text":"Animate freely: "},{"text":"transform","code":true},{"text":" (translate, scale, rotate, skew), "},{"text":"opacity","code":true},{"text":", and "},{"text":"filter","code":true},{"text":", though filters cost GPU time and can be slow on weak hardware."}]},{"t":"p","runs":[{"text":"Avoid animating: "},{"text":"width","code":true},{"text":", "},{"text":"height","code":true},{"text":", "},{"text":"top","code":true},{"text":", "},{"text":"left","code":true},{"text":", "},{"text":"right","code":true},{"text":", "},{"text":"bottom","code":true},{"text":", "},{"text":"margin","code":true},{"text":", "},{"text":"padding","code":true},{"text":", "},{"text":"border-width","code":true},{"text":", "},{"text":"font-size","code":true},{"text":": all trigger layout."}]},{"t":"p","runs":[{"text":"Avoid animating: "},{"text":"box-shadow","code":true},{"text":", "},{"text":"background-position","code":true},{"text":", "},{"text":"border-radius","code":true},{"text":", "},{"text":"color","code":true},{"text":": no layout, but a repaint every frame. Sometimes acceptable for a small element; noticeably expensive on a large one. A moving "},{"text":"box-shadow","code":true},{"text":" 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."}]},{"t":"h2","id":"layers-are-not-free","runs":[{"text":"Layers are not free"}]},{"t":"p","runs":[{"text":"will-change: transform","code":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"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 "},{"text":"worse","i":true},{"text":" performance, or crashes. The rules that follow from the mechanism:"}]},{"t":"ul","items":[[{"text":"Add "},{"text":"will-change","code":true},{"text":" shortly before the animation and remove it after."}],[{"text":"Never put it on a long list of elements “just in case”."}],[{"text":"If you find yourself adding it everywhere, the problem is elsewhere."}]]},{"t":"h2","id":"seeing-it-rather-than-guessing","runs":[{"text":"Seeing it rather than guessing"}]},{"t":"p","runs":[{"text":"Open DevTools → the three-dot menu → More tools → Rendering, and turn on "},{"text":"Paint flashing","b":true},{"text":". 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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"the-one-exception-worth-knowing","runs":[{"text":"The one exception worth knowing"}]},{"t":"p","runs":[{"text":"The Web Animations API with "},{"text":"element.animate()","code":true},{"text":" can hand a transform or opacity animation straight to the compositor, exactly like CSS. A "},{"text":"requestAnimationFrame","code":true},{"text":" loop that sets "},{"text":"style.transform","code":true},{"text":" each frame cannot: it is main-thread by definition, because your JavaScript has to run to produce each value."}]},{"t":"p","runs":[{"text":"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 "},{"text":"ScrollTimeline","code":true},{"text":" move a large part of that case back onto the compositor."}]},{"t":"h2","id":"the-free-path","runs":[{"text":"The free path"}],"role":"free"},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"the-limitation","runs":[{"text":"The limitation"}],"role":"limit"},{"t":"p","runs":[{"text":"“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 "},{"text":"grid-template-rows: 0fr → 1fr","code":true},{"text":", which does trigger layout but on a much smaller subtree than the whole page."}]},{"t":"p","runs":[{"text":"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."}]}],"check":{"question":[{"text":"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?"}],"options":[{"text":[{"text":"Width animations are interpolated in JavaScript by the browser’s animation engine, while transforms are interpolated in CSS, and only the CSS path is exempt from main-thread blocking."}],"correct":false,"tempting":[{"text":"Attributes the difference to where the animation was declared rather than to which pipeline stages the property forces."}]},{"text":[{"text":"A width animation needs layout recalculated every frame, and layout runs on the main thread; a transform only changes how an existing layer is composited, which the compositor thread can do alone."}],"correct":true,"tempting":null},{"text":[{"text":"The transform animation is being run by the GPU, which caches the whole animation in advance, so it would continue even if the page were closed mid-flight."}],"correct":false,"tempting":[{"text":"Overstates compositor independence into a claim that the animation is precomputed and detached from the document."}]},{"text":[{"text":"The width animation has triggered a repaint of the element’s text, and text rasterisation is the only operation in the rendering pipeline that is pinned to the main thread while every other stage can be parallelised onto worker threads."}],"correct":false,"tempting":[{"text":"Singles out text rasterisation; layout itself is the main-thread bottleneck, and paint is not the distinguishing factor here."}]}],"order":[3,2,0,1],"why":null},"prev":{"slug":"prefers-reduced-motion","title":"Motion that makes people ill","minutes":10},"next":{"slug":"which-technology","title":"Five ways to put motion on a screen","minutes":10}}}