Skip to content

GPU Compositing: transform vs left/top

advanced14 min read

Two Animations, Wildly Different Performance

Pop quiz before we even start: these two CSS animations produce the exact same visual result — moving a box 300px to the right. One runs at 60fps on a budget phone. The other drops frames on a high-end desktop. Can you guess which is which?

/* ❌ Animates left — triggers layout + paint + composite every frame */
@keyframes move-left {
  from { left: 0; }
  to { left: 300px; }
}

/* ✅ Animates transform — triggers composite only */
@keyframes move-transform {
  from { transform: translateX(0); }
  to { transform: translateX(300px); }
}

Same pixels, wildly different performance. Understanding why requires knowing how the browser's compositing architecture works.

Mental Model

The browser has two threads that work together: the main thread (runs JavaScript, calculates styles, performs layout, generates paint commands) and the compositor thread (takes painted layers, applies transforms and opacity, composites the final frame, sends to GPU). Animations that only need the compositor thread bypass the main thread entirely. The main thread could be running a 200ms JavaScript task, and compositor-only animations still run at 60fps because they are on a different thread.

The Full Pipeline vs Compositor-Only

When you animate left:

Every frame:
  Main Thread: Style → Layout → Paint → [hand off]
  Compositor Thread: Composite → GPU → Screen

When you animate transform:

Every frame:
  Main Thread: (nothing — not involved)
  Compositor Thread: Apply transform to existing texture → GPU → Screen
Execution Trace
left animation
Frame N: left changes from 150px to 151px
Main thread recalculates layout (element moved, siblings may shift), repaints the layer, hands new texture to compositor
left cost
Style (0.5ms) + Layout (2ms) + Paint (1.5ms) + Composite (0.5ms)
~4.5ms per frame on main thread. Blocks JS execution.
transform animation
Frame N: translateX changes from 150px to 151px
Compositor takes the existing GPU texture and moves it. Main thread is never involved.
transform cost
Composite only (0.3ms on compositor thread)
~0.3ms, off the main thread. JavaScript continues unblocked.

Compositor-Only Properties

So which properties get this special treatment? Only four CSS properties can be animated entirely on the compositor thread:

PropertyWhat the compositor does
transformMove, rotate, scale, skew the layer's GPU texture
opacityAdjust the layer's alpha channel
filterApply GPU-accelerated visual effects (blur, brightness, etc.)
backdrop-filterApply filters to content behind the element

Everything else — left, top, width, height, margin, padding, border, font-size, color, background — requires the main thread for either layout recalculation, repaint, or both.

Quiz
You need to animate an element's size from 100px to 200px width. Which approach avoids main thread work?

How GPU Textures Work

To understand why this is fast, you need to know what happens under the hood. When an element is promoted to its own compositing layer, the browser:

  1. Paints the element into a bitmap (rasterization)
  2. Uploads that bitmap to the GPU as a texture
  3. Stores the texture in GPU memory (VRAM)
  4. Composites by positioning textures relative to each other

The texture is basically a screenshot — once painted and uploaded, the compositor can move, rotate, scale, or fade it without ever repainting. This is the key insight behind why transform and opacity are fast: the compositor just shuffles existing textures around rather than generating new ones.

Main Thread                    GPU Memory
┌──────────┐                  ┌──────────────────┐
│ Paint     │ ──rasterize──→ │ Texture: Layer 1  │
│ element   │    upload       │ (bitmap of pixels)│
└──────────┘                  └──────────────────┘

Compositor Thread             GPU
┌──────────┐                  ┌──────────────────┐
│ Transform │ ──move/scale──→ │ Draw texture at   │
│ the layer │   the texture   │ new position      │
└──────────┘                  └──────────────────┘
Common Trap

Each GPU texture consumes memory proportional to the element's pixel dimensions. A 1000x1000 element at 2x device pixel ratio creates a 2000x2000 texture = 16MB of GPU memory (4 bytes per pixel * 4 million pixels). Promoting too many elements to layers can exhaust GPU memory, causing the browser to fall back to software rendering — which is far slower than not having layers at all.

Layer Creation: What Gets Promoted

The browser promotes elements to their own compositing layers when:

/* Explicit promotion */
.promoted {
  will-change: transform;  /* Tells browser: I'll animate this */
  transform: translateZ(0); /* Old hack, same effect */
}

/* Implicit promotion */
.video-element { }          /* <video> elements */
.canvas-element { }         /* <canvas> elements */
.has-3d-transform {
  transform: rotate3d(1, 0, 0, 45deg);  /* Any 3D transform */
}
.fixed-element {
  position: fixed;           /* Fixed positioning (in many browsers) */
}
.animated {
  animation: slide 1s;       /* Active CSS animation on compositor property */
  transition: transform 0.3s; /* Active CSS transition on compositor property */
}

Elements also get promoted when they overlap a composited layer and the browser cannot determine stacking order without a separate layer (squashing heuristic).

Quiz
An element has position: absolute and you animate its top property. Does it run on the compositor thread?

Practical Animation Comparisons

Slide-In Animation

/* ❌ Layout every frame */
.panel-left {
  position: absolute;
  transition: left 0.3s ease;
  left: -300px;
}
.panel-left.open {
  left: 0;
}

/* ✅ Compositor only */
.panel-transform {
  transform: translateX(-100%);
  transition: transform 0.3s ease;
}
.panel-transform.open {
  transform: translateX(0);
}

Fade + Scale Entrance

/* ✅ All compositor properties */
.card-enter {
  opacity: 0;
  transform: scale(0.95) translateY(10px);
  transition: opacity 0.2s, transform 0.3s ease-out;
}
.card-enter.visible {
  opacity: 1;
  transform: scale(1) translateY(0);
}

Expanding Card (Layout Required)

Sometimes you genuinely need layout to change — surrounding elements must reflow:

/* When layout change is intentional, minimize the damage */
.expandable {
  contain: layout style;  /* Limit reflow scope */
  transition: height 0.3s ease;
  overflow: hidden;
}

If layout change is only visual (no sibling reflow needed), use scale instead:

.expandable-visual {
  transform-origin: top left;
  transition: transform 0.3s ease;
}
.expandable-visual.expanded {
  transform: scaleY(1.5);
}
Why does the compositor thread exist?

Early browsers did everything on the main thread — JavaScript, layout, paint, and compositing. When JavaScript ran for 100ms, scrolling froze. Chrome introduced the compositor thread (Project CC, around 2012) specifically to decouple scrolling and compositor animations from the main thread. The compositor can independently:

  • Handle scroll events and update scroll position
  • Run transform/opacity animations
  • Manage touch gestures (pinch-to-zoom, overscroll bounce)

This architecture is why Chrome can scroll smoothly even while JavaScript is busy — the compositor thread processes the scroll independently and recomposites with the new scroll offset. Other browsers followed the same architecture.

Measuring the Difference in DevTools

Rendering Tab

  1. Open DevTools → More tools → Rendering
  2. Enable "Paint flashing" — green overlays show repainted areas
  3. Enable "Layer borders" — orange borders show composited layers

With left animation: green flash on every frame (repainting). With transform animation: no green flash (compositor only, no repaint).

Performance Panel

Record the animation and compare:

  • left: Style, Layout, Paint, and Composite events every frame
  • transform: Only Composite events — Style, Layout, and Paint are absent
Quiz
You enable 'Paint flashing' in DevTools. A CSS transition shows green flashing on every frame. What does this tell you?

The Cost Hierarchy

Not all CSS properties are created equal when it comes to animation cost:

Cheapest → Most Expensive:

1. transform, opacity    → Composite only (compositor thread)
2. color, background     → Paint + Composite (main thread, no layout)
3. font-size, padding    → Layout + Paint + Composite (main thread, full pipeline)
4. top/left + siblings   → Layout (affecting many elements) + Paint + Composite
CSS Triggers reference

Paul Lewis's csstriggers.com catalogs every CSS property and which pipeline stages it triggers in each browser engine (Blink, Gecko, WebKit). Consult it when choosing animation properties.

Key Rules
  1. 1Only transform, opacity, filter, and backdrop-filter are compositor-only — they skip layout and paint entirely.
  2. 2Compositor-only animations run on the compositor thread, separate from the main thread. They stay smooth even during long JavaScript tasks.
  3. 3Animating left/top triggers layout recalculation every frame, even for absolutely positioned elements.
  4. 4GPU textures consume memory proportional to element dimensions × device pixel ratio. Each layer costs VRAM.
  5. 5Use DevTools Paint Flashing to identify non-compositor animations. Green = repaint happening.
  6. 6When layout change is only visual (no sibling reflow), use transform: scale() instead of width/height.
  7. 7The compositor thread also handles smooth scrolling and touch gestures independently of JavaScript.
Interview Question

Q: Explain why animating transform: translateX(100px) performs better than animating left: 100px. Be specific about the browser architecture involved.

A strong answer covers: the main thread vs compositor thread architecture, the rendering pipeline stages (style → layout → paint → composite), how left triggers layout recalculation and repaint on the main thread every frame, while transform only requires the compositor to reposition an existing GPU texture. Mention that compositor-only animations continue running during long JavaScript tasks because they are on a different thread. Bonus: explain GPU texture rasterization, the memory cost of layers, and implicit layer promotion for overlap stacking order resolution.