Summary
The browser rendering pipeline has four stages: style, layout, paint, and composite. Animating properties that trigger layout or paint on every frame destroys frame rate. Restricting motion to `transform` and `opacity` lets the compositor thread run animations off the main thread at 60 fps, even while JavaScript is busy.
Jump to the interview angleCompositor thread
The browser splits rendering into a main thread (style, layout, paint) and a compositor thread (compositing layers into the final frame). The compositor thread can animate transform and opacity without touching the main thread at all — which means those animations run at 60 fps even during a JavaScript-heavy task. Any other animated property (color, width, top, left) forces the main thread to repaint before the compositor can proceed.
transform vs top/left — and content-visibility
Two patterns side by side: replacing a layout-triggering animation with a compositor-only one, then using content-visibility: auto to skip paint for off-screen sections entirely.
/* ❌ triggers layout on every frame */
.card-bad {
transition: top 300ms ease, left 300ms ease;
}
/* ✓ compositor-only: no layout, no paint */
.card-good {
transition: transform 300ms ease;
}
/* Promote an element to its own layer when you know it will animate */
.drawer {
will-change: transform;
/* The browser allocates a GPU texture for this element now.
Remove will-change once the animation ends to free the memory. */
}
/* Skip painting off-screen sections entirely */
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 300px; /* estimated height to hold scroll position */
}
/* CSS containment — tell the browser a subtree is independent */
.widget {
contain: layout style paint;
/* layout: children cannot affect outside geometry
style: counters/quotes don't escape
paint: contents are clipped and not painted outside the box */
}transform and opacity run on the compositor thread. content-visibility: auto skips rendering off-screen sections, cutting the rendering cost of off-screen content substantially on long pages. contain narrows the scope of style/layout recalculations.
Rules for jank-free animation
- Animate only `transform` and `opacity` — both are compositor-only properties.
- `will-change: transform` promotes an element to its own GPU layer before animation starts.
- `will-change` costs GPU memory; apply it just before animation and remove it after.
- `content-visibility: auto` skips layout and paint for off-screen content sections.
- `contain: layout style paint` isolates a subtree so changes inside it don't retrigger outside.
- A "paint storm" occurs when a single state change invalidates paint for the whole page; containment limits the blast radius.
will-change is not free
Every element with will-change: transform gets its own GPU texture — even when idle. Slapping it on dozens of cards inflates GPU memory and can cause jank on low-end devices. Apply it dynamically (add the class on mouseenter, remove on animationend) rather than statically in a stylesheet.
Interview angle
Interviewers test whether you know which CSS properties bypass layout and paint. Name the compositor thread, explain why top/left trigger layout while transform: translate() does not, and call out will-change memory cost.
Soundbite: "Only transform and opacity are compositor-only — everything else forces a repaint at minimum."
Key terms
- compositor thread
- Browser thread that combines GPU layers into the final frame, independent of the main thread.
- will-change
- CSS hint that promotes an element to its own GPU layer before animation, at a memory cost.
- content-visibility: auto
- Skips layout and paint for off-screen elements; browser re-renders them when they scroll into view.
- CSS contain
- Property that limits how much a subtree's changes affect the rest of the document's layout and paint.
- paint storm
- A single DOM or style change that invalidates and repaints a large, uncontained region of the page.