Why transform and opacity are GPU-accelerated
The Architecture
The browser rendering pipeline has two threads relevant to visual output: the main thread and the compositor thread. The main thread runs JavaScript, resolves styles, computes layout, and generates paint records. The compositor thread takes those paint records, rasterizes them into GPU textures (often in cooperation with raster worker threads), and submits finished frames to the display. This is the foundation behind Compositing and GPU Acceleration; it builds on Transform and Opacity Best Practices.
transform and opacity are GPU-accelerated because they can be applied by the compositor thread without re-running any main-thread work. Once an element’s content has been rasterized into a GPU texture, the compositor can:
- Apply a
transformby multiplying the layer’s draw matrix — a single GPU operation taking under 1ms. - Apply
opacityby adjusting the layer’s alpha blending factor — equally cheap.
Neither operation requires re-evaluating CSS rules, re-running layout, or re-rasterizing pixels. The GPU texture from the previous frame is reused; only the transformation matrix or blend factor changes. This is why these two properties are the foundation of smooth animation.
All other visual properties — width, height, left, top, color, background-color, border-radius when changed dynamically, and so on — require at least a repaint (new rasterization) and often a full layout recalculation before the compositor can draw the updated frame. To put this offload to work, see Animating transforms without layout thrash for the JS-loop pattern and Scroll and Input Performance for keeping gesture-driven motion on the compositor.
When It Breaks: Implicit Layout Recalculation
Intermittent frame drops during transform and opacity-only animations indicate that the compositor is being forced to involve the main thread. DevTools traces show unexpected Layout or Recalculate Style spikes alongside the animation frames.
Common root causes:
- Nested
position: relativeorposition: absoluteancestors that change their dimensions when the animated element moves. This forces a layout recalculation that propagates up the tree. - Dynamic
z-indexmutations during the animation. Changingz-indexcan change the stacking order, which forces the compositor to rebuild part of the layer tree. - Conflicting
will-changedeclarations on ancestor elements that cause unexpected layer promotion/demotion mid-animation. - Static
will-changeon many elements that exhausts GPU memory and triggers layer eviction, forcing re-rasterization.
Debugging Protocol
- Capture a trace: DevTools → Performance. Record 5 seconds of the animation. Filter for
layout paint composite. - Check for unexpected events: In a compositor-only animation, the Main thread lane should show only a thin
Composite Layersentry. AnyLayout,Recalculate Style, orUpdate Layer Treeevent in the Main thread lane during the animation indicates the GPU offload path was broken. - Enable visual overlays: Rendering tab → Layer borders and Paint flashing. Shifting blue borders indicate layers being created/destroyed mid-animation. Yellow flashes indicate repaints that should not be happening.
- Audit parent containers: Use the Styles pane to trace ancestor elements for layout-triggering properties. Check for
width,padding,top,marginon any ancestor that wraps the animated element. - Audit
will-changedeclarations: Search the stylesheet for staticwill-changeapplied broadly. Remove declarations from elements that are not actively animating and add them dynamically only during the animation window.
// DevTools Performance: filter expression to find pipeline invalidations
// In the search box of the flame chart:
// Look for: Layout, RecalculateStyle, UpdateLayerTree events
// with duration > 2ms during animation frames
The trace below shows the tell: a clean compositor animation keeps the Main lane empty except for a thin Composite Layers sliver, so any Layout block interleaved with the frames is the anomaly to chase.
Mitigation Patterns
React: Compute final geometry in useLayoutEffect before applying animation classes. Do not mutate inline styles during render; use CSS transitions triggered by state-derived class names.
Vue: Use @vueuse/core useRafFn to batch property updates within animation frames. Ensure transition wrapper components do not inject conflicting inline transform overrides.
Angular: Use NgZone.runOutsideAngular() for scroll-linked animation loops to bypass change detection overhead. Apply transform via Renderer2.setStyle() rather than direct DOM property writes to keep Angular’s internal model consistent.
Verification
| Metric | Target |
|---|---|
Layout events during animation |
0 per frame |
Paint events during animation |
0 per frame |
Compositor BeginMainFrame interval |
≤ 16.6ms sustained |
| Active compositor layers | < 100 on mobile |
| TBT and INP | Within 5% of pre-animation baseline |
Use chrome://tracing with cc and input categories to confirm BeginMainFrame intervals are stable and that InputLatency::GestureScrollUpdate shows no delays during the animation. For turning these checks into recorded, alertable metrics, see Rendering Performance Metrics and Tooling.
The Texture-Reuse Insight
The reason transform and opacity are cheap comes down to one fact: neither changes the pixels inside the element, only how an already-rasterized texture is placed on screen. Once the compositor has a layer’s content baked into a GPU texture, applying a new transform is a matrix multiply the GPU performs for free on every frame, and applying a new opacity is an alpha blend it also does natively. No box is re-measured, no display list is re-recorded, no tile is re-rasterized. The work happens entirely in the compositor’s assembly step, which runs on its own thread, and that is why these animations survive a fully blocked main thread — the thing advancing them never touches the main thread at all.
Contrast this with what the GPU cannot shortcut. A width change alters geometry, so the engine must re-run layout and then repaint the element into a new texture before the compositor can even begin; a box-shadow change repaints a blurred region. Both re-enter the main-thread pipeline every frame. The GPU is happy to composite whatever textures it is given, but it cannot invent a texture for a size or shape the main thread has not yet painted — so the acceleration is real only for the two properties that leave the texture’s contents untouched. Understanding it as texture reuse, rather than a vague “the GPU is fast,” is what makes it obvious why the list of compositor-friendly properties is so short and why substituting a translate for a left animation is not a micro-optimisation but a category change in cost.
Frequently Asked Questions
Why are transform and opacity GPU-accelerated but left and top are not?
transform and opacity only change how an already-rasterized layer is drawn — the compositor multiplies a matrix or scales an alpha factor and reuses the previous frame’s GPU texture. left and top change an element’s box geometry, which invalidates layout and forces a repaint before the compositor has anything new to draw.
Do transform and opacity animations always stay on the compositor thread?
No. They stay off the main thread only when nothing else invalidates layout or the layer tree during the animation. A z-index mutation, a resizing ancestor, or will-change churn can pull the work back onto the main thread, which shows up as Layout or Update Layer Tree events in the trace.
How do I confirm an animation is running purely on the compositor?
Record five seconds in DevTools Performance and inspect the Main lane. A clean compositor animation shows only a thin Composite Layers sliver there; any Layout, Recalculate Style, or Update Layer Tree block interleaved with the frames means the GPU offload path broke.
Does promoting an element with will-change make transforms faster?
It can, by giving the element its own composited layer ahead of time, but only when applied narrowly and temporarily. Static will-change on many elements exhausts GPU memory and triggers layer eviction and re-rasterization — the opposite of the intended effect.
Related Guides
- Animating transforms without layout thrash — the JS-loop pattern that keeps transform writes off the layout path.
- Layer Promotion and Composition — how the compositor decides which elements get their own GPU texture.
- Compositor Thread and Rasterization — where paint records become the textures transform and opacity reuse.
- Transform and Opacity Best Practices — the parent guide covering the full property-choice discipline.