The compositor thread: animating without the main thread
The browser runs a second thread whose only job is to assemble already-painted layers into a frame, and when an animation touches only transform or opacity, the browser can hand the entire thing to that thread — which is the real, mechanical reason it keeps running smoothly even while your JavaScript is busy blocking everything else.
The compositor thread: animating without the main thread
Go back to the deadline from the first lesson in this module: every 16.6ms, the browser has to hand the display a finished frame, and most of the work that produces that frame — your JavaScript, style, layout, paint — runs on a single main thread. If that thread is busy for 50ms doing something else, no frame gets produced for 50ms. That should mean every animation freezes the instant the main thread is blocked. In practice, it doesn't. Open a page with a CSS transform transition running, block the main thread with a long synchronous loop, and the transform keeps gliding smoothly across the screen the entire time — while a left-based transition on the same page freezes solid until the loop finishes.
That split is not a coincidence or a browser trick. It's the visible evidence of a second thread that the previous lesson's pipeline diagram left out entirely: the compositor thread. Some animations run there instead of on the main thread, and once you see why, "animate transform and opacity" stops being advice you follow and becomes a fact you can derive.
Two threads, two jobs
The main thread is the one you already know. It runs your JavaScript, and it's also where the browser does style recalculation, layout, and paint — the first three stages of the pipeline from Layout, paint, composite. Anything that needs to know "what does this element look like now" or "where exactly does this box sit" has to go through the main thread, because that's where the DOM, the style rules, and the layout algorithm live.
The compositor thread is a separate thread — sometimes backed further by a dedicated GPU process — whose entire job is the last stage of that pipeline: composite. It doesn't know what a <div> is. It doesn't parse CSS. It doesn't run your event handlers. What it holds is a set of already-painted layers — bitmaps, effectively textures sitting in GPU memory — along with instructions for how to position, scale, rotate, and blend them relative to each other. Every frame, its job is to take those textures, apply the current transform and opacity value for each one, and hand the GPU an assembled image. That operation — moving and blending pre-rendered bitmaps — is exactly the kind of embarrassingly parallel, no-CPU-logic-required task a GPU is built for, and it's why the compositor can run this step even while the main thread is completely occupied with something else.
Note what the compositor thread is not: it isn't a second copy of the browser's rendering engine that can do layout or paint in parallel. It can only work with layers the main thread has already painted and handed to it. It composites; it doesn't render from scratch. That limit is what decides which animations qualify for the handoff and which don't.
The handoff: what makes an animation compositor-only
Recall the table from the layout/paint/composite lesson: transform and opacity are the two properties whose effect is purely a composite-stage concern — they change neither an element's geometry nor its painted pixels, only how an already-painted bitmap gets placed or blended. That fact has a second, bigger consequence beyond "it's cheaper per frame": if an animation only ever touches transform and/or opacity for its entire duration, the browser can determine, up front, that no frame of that animation will ever need style recalculation, layout, or paint. Every single frame is answerable with information the compositor already has.
When the browser can establish that in advance, it does something more aggressive than "skip layout and paint this frame." It hands the entire animation — the timing function, the start and end values, the duration — to the compositor thread as a self-contained instruction: "starting now, interpolate this layer's transform/opacity along this curve, once per frame, for this many milliseconds, and don't wait for me." The main thread doesn't need to be asked again until the animation finishes. It isn't ticking the animation forward frame by frame; it delegated the whole thing and moved on.
This is the mechanism behind the demonstration at the top of this lesson. A transform transition running entirely on the compositor thread has no dependency on the main thread being free — it was scheduled once, and the compositor advances it on its own clock, using its own textures, regardless of what the main thread is doing. A left transition cannot get that treatment, because left is a layout input: every single frame, the browser must recompute geometry (main thread), re-rasterize pixels (main thread), and only then hand a new bitmap to the compositor. If the main thread is stuck running your 50ms loop, none of that can happen — style, layout, and paint are queued up behind your code — so the left animation doesn't just run slowly, it doesn't advance at all until the thread frees up.
This also resolves something that might feel like a loose end from the first lesson in this module: the 16.6ms deadline was framed entirely in terms of main-thread work. A compositor-driven animation is how you keep meeting that deadline even when the main thread is missing it — the compositor has its own, much lighter, per-frame job, and it can hit its own deadline independently. It's also why an eased transform transition — the kind covered in Easing under the hood — actually delivers the smooth curve it promises under real-world load: the easing math for a compositor-run animation is evaluated on the compositor's schedule, not competing with your app's JavaScript for main-thread time. An eased left transition has no such guarantee; its curve is only as smooth as the main thread's ability to keep up.
Layers and promotion: how a texture comes to exist
None of this works unless an element has already been painted onto its own layer before the animation needs to reposition it. An element doesn't get a dedicated layer by default — most elements are painted together into a shared layer with their neighbors, because giving every element its own layer would be wasteful. An element gets promoted to its own compositor layer when the browser has a specific reason to isolate it: it has a 3D transform, it's the target of a compositor-run animation, it has will-change: transform (or opacity) applied, or a few other triggers covered in the layout/paint/composite lesson.
Promotion is what turns "an element painted as part of a larger layer" into "a standalone texture the compositor can move independently without repainting anything around it." That's also why will-change matters here specifically: it lets you request the promotion before the animation starts, so the one-time cost of allocating that layer and painting it in isolation lands during idle time instead of showing up as a dropped first frame right when your transition begins. If you don't hint the browser and the animation is compositor-eligible, the browser will still promote the element automatically once it detects a compositor-run transform/opacity animation is starting — but you lose the ability to control when that one-time cost is paid.
The limits: layers aren't free, and "looks cheap" isn't proof
It's tempting to read all of this as "so I should just promote everything to its own layer and animate transform everywhere," but the compositor's speed comes from a resource that isn't infinite: GPU memory. Every promoted layer is a texture sitting in video memory, and every additional layer adds bookkeeping the compositor has to do to keep them all correctly stacked, clipped, and blended. Promote a hundred elements that don't actually need independent movement, and you can flip the bottleneck from "main thread busy" to "GPU memory pressure and layer-management overhead" — which produces its own kind of jank, just from a different cause. This is the same warning the layout/paint/composite lesson raises about will-change: it's a scalpel for elements you know are about to move, not a blanket switch.
The second trap is assuming a property is compositor-eligible because it feels visual and lightweight. background-color, border-radius, box-shadow, and filter all look like they shouldn't need much work, but most of them still require a repaint of the affected layer's pixels — they change what gets drawn, even though they don't change geometry. A repaint is main-thread work (or at least work that has to happen before the compositor has a new texture to place), so an animation built on those properties is still subject to everything in this lesson's first section: it competes with your JavaScript for main-thread time, frame by frame, for its entire duration. transform and opacity remain the narrow, genuinely-special case — not because the browser arbitrarily blessed them, but because they're the only two common properties whose entire effect lives at the composite stage where the second thread can operate alone.
Where this goes next
You now have the full explanation for a piece of advice you've probably followed without deriving: animate transform and opacity because doing so hands the entire animation to a thread that isn't contending with your JavaScript, your layout, or your paint work for the frame deadline — and animating almost anything else keeps you pinned to the main thread, subject to every jank source the earlier lessons in this module describe.
The next lesson, The Web Animations API, looks at the actual engine that decides all of this under the hood — the same Animation object model that backs CSS transitions, CSS @keyframes, and JavaScript's element.animate() — and how you can inspect and control an animation, including a compositor-run one, directly from JavaScript.
Go deeper
- Chrome Developers — Inside look at modern web browser (part 3) — Chromium's own explanation of the compositor thread, layers, and why compositing can run independent of the main thread.
- web.dev — Stick to Compositor-Only Properties and Manage Layer Count — The authoritative source on which properties qualify for compositor-only animation, and the layer-count cost warning this lesson raises.
- MDN — will-change — The precise semantics of layer promotion via will-change, including the guidance against applying it preemptively to elements that aren't about to animate.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- What kind of work does the compositor thread do, and what kind of work can it never do on its own?
- Explain mechanically why a transform transition keeps running when the main thread is blocked by a long synchronous loop, while a left transition freezes.
- What has to be true about an animation's entire duration — not just its current frame — before the browser will hand the whole thing to the compositor thread?
- What does 'promoting an element to its own compositor layer' actually give you, and how does will-change let you control when its one-time cost is paid?
- A teammate assumes background-color is compositor-only because 'the GPU handles colors.' What's the actual mechanical reason that assumption is wrong?
- Why can having too many compositor layers become its own performance problem, rather than the compositor always being a free win?
- Tie this lesson back to the frame deadline from lesson 1: in what specific sense does a compositor-run animation 'keep hitting' a 16.6ms deadline that the main thread is missing?