The Rule Everyone Repeats Wrong
Never block the main thread. You have heard it, I have repeated it, and it is close enough to true that it survives unexamined for years.
Victor Ayomipo pulled at it in a piece for Smashing Magazine and landed somewhere more useful: never block the main thread for too long. That is not a softening. It is a different instruction, because it turns an absolute into a budget, and budgets can be measured.
For a motion designer, this matters more than it does for most web developers. Your animation lives on that thread unless it happens to be one of the properties the compositor handles. Every choice about where computation happens is, indirectly, a choice about whether your easing curve holds.
Three Numbers Worth Memorising
The budget has edges, and they are specific:
- 16.6ms is the frame. At 60fps the browser has that long to paint. Miss it and you have dropped a frame, which a viewer reads as a stutter rather than as a number.
- 50ms is where a task officially becomes a long task. Past that, input feels unresponsive rather than merely slow.
- Roughly 1 second of blocking can be defensible after an explicit user action, because the user has a mental model of having asked for something.
That last one is the interesting concession. There is a real difference between an interface that freezes while you scroll, which feels broken, and one that pauses briefly after you clicked "export", which feels like work happening. Same technical event, entirely different reading.
Why Moving Work Off-Thread Can Cost More
Here is the part that changed how I think about it.
Ayomipo built a Chrome screenshot extension and did the correct thing: pushed image processing into an Offscreen Document so the main thread stayed free. The result was a 2 to 3 second lag. The image payload was over a megabyte of Base64, and shipping it across contexts meant JSON serialisation in both directions. The transfer dwarfed the work.
His fix was to process on the main thread in the content script. Fewer round trips, and device pixel ratio calculations became straightforward because he was already in the right context.
The framework he draws out of it is worth stealing:
- Compute-heavy work is CPU bound. Transfer costs are small relative to processing. Move it off-thread and you win.
- Data-heavy work is dominated by the size of what you are passing. Move it off-thread and you pay for the move twice, once out and once back.
Most motion-adjacent work I do falls into the second category more often than I expected. Reading pixel data off a canvas, measuring a large DOM subtree, preparing keyframes from a big JSON payload. The computation is trivial. The data is not.
What This Means For Animation Specifically
If you are animating transform and opacity, the compositor handles it and main thread congestion matters less. That is the whole reason those two properties get recommended so relentlessly.
Everything else is exposed. Animate width, top, filter or anything that triggers layout, and your animation is queueing behind whatever else the main thread is doing. A 200ms task in the middle of a 300ms transition does not slow the transition down. It removes twelve frames from it.
So the practical question during a motion build is not "is this blocking?" but "is this blocking while something is moving?". Those are different problems with different fixes:
- Blocking before the animation starts is usually fine, and often better than a spinner.
- Blocking during it is never fine, regardless of how briefly.
- Blocking after it completes is fine and nobody will notice.
That reframing does more for real work than any amount of moving code into workers.
Measure Before You Move Anything
The advice I would actually give: do not guess at which category your task is in. Ayomipo recommends performance.mark() and performance.measure(), and it takes about a minute to wire up.
performance.mark("transfer-start");
worker.postMessage(payload);
// in the worker's message handler, back on the main thread:
performance.mark("transfer-end");
performance.measure("transfer", "transfer-start", "transfer-end");
console.table(performance.getEntriesByType("measure"));
Compare that number against the time the processing itself takes. If transfer is the larger figure, moving the work off-thread has made things worse, and no amount of architectural correctness will change that.
I would run this before refactoring anything into a worker, because "put it in a worker" is one of those recommendations that sounds unarguable and occasionally makes an interface slower.
An Opinion You Can Disagree With
Blanket performance rules survive because they are safe to repeat, not because they are right. "Never block the main thread" is in the same family as "never use !important" and "always use semantic HTML": correct often enough that repeating it costs you nothing, and wrong often enough that following it blindly produces worse work.
The version you should carry into a motion build is duller and more useful. You have 16.6ms per frame. Spend it deliberately. Know whether your bottleneck is compute or data before you move it anywhere. And treat blocking during an animation as the one hard rule, because that is the case where the user sees the cost rather than merely waiting for it.
If you want the counterpart to this on the CSS side, the Chrome 150 motion features piece covers what the browser now handles for you, which is the cheapest way to keep work off the main thread: not doing it there in the first place.