A browser tab's main thread is blocked by a long synchronous JavaScript task. A CSS `transform` animation keeps moving smoothly while a JavaScript-driven animation of the same element freezes. Why?
answer
- more than one thread renders
- the texture already exists
- the curve was handed over
- script runs where the block is
- what forces per-frame recompute
basics
~20 sThe CSS animation was handed to the compositor thread, which owns the layer's texture and can tick the transform and draw frames without the main thread. The JavaScript animation writes styles from main-thread callbacks, so while that thread is blocked nothing updates.
solid answer
~50 sBrowsers run rendering across several threads. The main thread runs JavaScript, style, layout and paint recording; a separate compositor thread holds the layer tree and draws frames on the GPU. When a CSS transition or animation targets only `transform` or `opacity` on a promoted element, the engine can hand the whole animation — the curve, the timing, the target values — to the compositor, which then ticks it every vsync without asking the main thread for anything. A blocked main thread cannot stop it. A JavaScript-driven animation is the opposite: whatever schedules it runs on the main thread and writes style there, so a blocked main thread means no new values and a frozen element. The practical rule is that only animations the compositor can own survive main-thread jank; anything that must recompute style, layout or paint each frame does not.
code
html · 14 lines<div id="box" style="width:80px;height:80px;background:#0af"></div>
<button id="block">Block main thread for 3s</button>
<script>
const box = document.getElementById('box');
box.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(300px)' }],
{ duration: 2000, iterations: Infinity, direction: 'alternate' }
);
document.getElementById('block').addEventListener('click', () => {
const end = Date.now() + 3000;
while (Date.now() < end) { /* busy */ }
});
</script>go deeper
Know that the browser renders on more than one thread, and that a CSS animation of transform or opacity can keep going while JavaScript on the main thread is busy.
Be ready to describe the handover: the engine gives the compositor the animation's curve and target values for a promoted layer, so frames are produced at vsync without consulting the main thread.
Demonstrate the failure cases — per-frame script, a non-compositable property mixed into the keyframes, contents changing each frame, oversized layers, and an animation that starts while the thread is already blocked — and use the effect as a diagnostic signal.
Own the tradeoff at design level: off-thread animation buys resilience to jank by giving up per-frame reactivity, so decide which motion in a product must survive a blocked main thread and constrain how it may be authored.
## The threads involved A modern browser does not render on one thread. Roughly: - **Main thread** — runs JavaScript, recalculates style, performs layout, records paint, and runs the rendering callbacks that scripts register. - **Compositor thread** — owns the layer tree, decides which tiles are needed, applies each layer's transform and opacity, and submits frames. - **Raster workers** — execute paint recordings into bitmaps. - **GPU process/thread** — issues the actual graphics commands. Only the first of these is blocked by a long synchronous task in page script. The others keep working with what they already have. ## How an animation gets handed over When a CSS transition, a CSS animation, or a Web Animations animation targets a property the compositor can apply by itself — in practice `transform` and `opacity`, and in some engines `filter` — and the element can be promoted, the engine offloads it. The compositor receives the animation's timing function, duration and keyframe values, and from then on it computes the current value at each vsync and redraws the existing texture with it. The main thread is not consulted per frame. This is why such animations are described as running "off the main thread", and why they survive jank that freezes everything else on the page. ## Why a script-driven animation cannot survive A JavaScript animation, however it is scheduled, ultimately does the same thing every frame: compute a value and write it into an element's style. Both the computation and the write happen on the main thread, and the style change then has to be processed by the main thread's rendering steps. If that thread is inside a four-second loop, no callback runs, no style is written, and the element does not move. The compositor may still be drawing frames — it just has nothing new to draw for that element. ```js // frozen while the main thread is busy: values are computed on it let x = 0; function step() { x += 2; box.style.transform = `translateX(${x}px)`; requestAnimationFrame(step); } step(); // survives a blocked main thread once handed to the compositor box.animate( [{ transform: 'translateX(0)' }, { transform: 'translateX(400px)' }], { duration: 2000, easing: 'linear' } ); ``` The second form is not magic because it uses a different API — it is offloadable because it declares the whole animation up front in terms of a compositable property, so the engine has something it can hand over. ## What pulls an animation back onto the main thread A candidate who only knows the happy path will claim `transform` is always off-thread. The cases that break it are the interesting part of the answer: - **Any per-frame script.** Computing values in a callback and assigning them is main-thread work by construction, no matter which property is assigned. - **A non-compositable property in the same animation.** If the keyframes also animate `width`, `top`, `background-color` or `box-shadow`, layout or paint must run each frame and the whole animation stays on the main thread. - **Contents that change each frame.** Promotion avoids re-recording a layer whose pixels are stable. If the layer's text or children are being rewritten, it must be repainted and re-rasterized regardless. - **Layers that are too large.** GPU texture size is bounded; a layer beyond the limit cannot be handled as a single texture and the engine falls back to work it can do. - **Starting the animation.** Offloading is set up by the main thread. An animation that begins while the main thread is already blocked cannot be handed over until that thread is free. ## Using the effect deliberately This is why loading indicators and skeleton shimmers are written as CSS `transform`/`opacity` animations: they keep moving during exactly the heavy work they exist to cover, whereas a script-driven spinner freezes at the moment it is most needed. It is also the diagnostic tell — if animation on a page stops dead but a pure CSS transform animation keeps going, the main thread is blocked rather than the GPU being saturated. The limits are worth stating plainly too: an off-thread animation cannot react to anything computed in script, cannot be driven by measured layout values, and cannot change layout. Smoothness under jank is bought by giving up per-frame control.
- Which threads keep working while the main thread is blocked by a long synchronous task?The compositor thread, the raster workers, and the GPU process. They continue with the layer tree and textures they already hold, so composited animations tick and previously rasterized content can still be drawn. Anything requiring new style, layout or paint waits for the main thread.
- Name a case where a `transform` animation still runs on the main thread.Several. Any animation whose values are computed in script each frame; keyframes that also animate a non-compositable property such as `width` or `background-color`; a layer whose contents change every frame and must be repainted; and an animation that starts while the main thread is already blocked, since offloading is set up there.
- Why are skeleton and loading animations usually written as pure CSS transform or opacity animations?Because they must keep moving during exactly the heavy main-thread work they exist to mask. Handed to the compositor, they tick through jank. A script-driven equivalent freezes at the moment the user most needs feedback that the page is alive.
- What do you give up by having an animation run on the compositor?Per-frame control. An offloaded animation is a fixed curve over compositable properties: it cannot read measured layout, cannot react to values computed in script mid-flight, and cannot change layout. Anything that must respond to state each frame has to stay on the main thread.
saying these in an interview costs you the question
- Believes GPU acceleration means script never blocks animation
- Claims any transform animation is automatically off-thread
- Thinks the compositor can run JavaScript callbacks
- Assumes a blocked main thread stops all rendering entirely
- Says CSS animations are faster simply because they are declarative