Layer Promotion & Compositor
To understand why some CSS animations feel buttery smooth while others stutter, it helps to know what the browser actually does between you writing a style change and pixels appearing on screen. That path runs through three broad stages: layout, paint, and composite — and which stage your change touches determines how expensive it is.
Layout → Paint → Composite
Stage | What happens | Triggered by |
|---|---|---|
Layout | The browser recalculates the size and position of every affected element on the page. | width, height, margin, position offsets, font-size, and other geometry-affecting properties. |
Paint | The browser fills in pixels for each box — text, colors, shadows, borders — onto one or more layers. | color, background, box-shadow, border-radius, and anything layout did not already invalidate. |
Composite | The GPU combines all painted layers into the final image shown on screen, applying any transforms/opacity along the way. | transform, opacity — and any property change on an already-promoted layer. |
What Promotes an Element to Its Own Layer
/* These changes stay on the compositor — cheap, GPU-driven */
.card {
transition: transform 0.2s ease, opacity 0.2s ease;
}
.card:hover {
transform: translateY(-4px) scale(1.02);
opacity: 0.95;
}
/* Hinting the browser to promote ahead of an upcoming animation */
.card {
will-change: transform, opacity;
}
/* Contrast: animating layout-affecting properties forces
layout + paint on every frame, on the main thread */
.card-slow {
transition: width 0.2s ease, margin-top 0.2s ease;
}
.card-slow:hover {
width: 220px; /* triggers layout every frame */
margin-top: -4px; /* triggers layout every frame */
}Once an element is on its own layer, animating transform or opacity on it only requires the GPU to recompute how that one texture is positioned or blended — the rest of the page's layers are untouched. That is the entire reason transform/opacity animations run smoothly even on lower-powered devices, while animating width, top, or margin can visibly drop frames: every frame of the animation re-triggers layout and paint across the affected region instead of a cheap GPU composite step.
Layer Explosion
/* Risky: promotes every card on the page permanently,
whether or not it is animating right now */
.card {
will-change: transform;
}
/* Better: only hint promotion right before the animation starts,
and remove the hint once it finishes (often via JS toggling a class) */
.card.is-animating {
will-change: transform;
}