Main Thread vs Compositor Thread
What runs on the main thread vs compositor, why scroll can be smooth while clicks feel dead, and fix strategies.
- browser
- main-thread
- compositor
- rendering
- performance
Browsers pipeline work across threads. The main thread (often “renderer main”) runs JS, style, layout, and much of paint setup. The compositor thread can scroll and animate already-promoted layers without waiting for JS — until something forces main-thread involvement.
Docs: web.dev rendering performance, Compositor thread (Chrome architecture series).
Who does what
| Work | Typical thread |
|---|---|
| JS (event handlers, React render) | Main |
| Style calc / layout | Main |
| Paint recording (often) | Main-ish / graphics pipeline |
| Layer composite / many scroll/transform anims | Compositor / GPU |
| Image decode | Often pool/decoder threads |
Mental model for interviews: JS and layout contend with input on main. Compositor-only animations stay smooth if layers are ready.
The classic paradox
“Scroll is 60fps but the button takes 500ms to react.”
Scroll can be compositor-driven on existing layers. The click still queues behind a long task on main. Smooth scroll ≠ good INP.
// Main thread blocked — compositor may still scroll cached layers
while (performance.now() < start + 300) {
/* bad busy work */
}
When scroll loses compositor-only status
- Scroll handlers calling
preventDefaultcarelessly - Non-passive touch listeners on document (historical jank)
- Constantly invalidating layout on scroll (
scroll→ measure → write) - Blur filters / complex effects that require main
window.addEventListener('touchstart', handler, { passive: true });
Animation strategy
/* Prefer compositor-friendly */
.drawer {
transform: translateX(0);
transition: transform 200ms;
}
Avoid animating height/top for primary motion. See Compositor layers.
Debugging
- Performance panel: main track busy during interaction?
- Frames drop only while JS runs?
- Layers panel: is the animated node promoted?
- Field: INP bad but CLS/LCP fine → main-thread interaction story.
Fix playbook
- Shorten main-thread JS (split, defer, yield).
- Move visual feedback earlier (optimistic UI, CSS).
- Keep scroll listeners passive and light.
- Virtualize lists so layout isn’t huge.
- Don’t block main with large JSON parse on click.
Interview out-loud
“Main thread runs JS, style, and layout; compositor can scroll and transform-animate promoted layers independently. That’s why scroll can be smooth while INP is terrible — fix long tasks and handler cost, not only CSS animations.”
Design system implication
Ship motion tokens that use transform/opacity by default. Ban layout-triggering animations in PR templates unless an a11y reduced-motion alternative exists. Document that “60fps marketing scroll” is not the same KPI as INP on checkout.
Related
Further depth
Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.
Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.
Further reading
Related guides
- Compositor LayersWhen browsers promote layers, why transform/opacity animate cheaply, and layer explosion costs.
- Browser DevTools Performance PanelRecord main-thread timelines: long tasks, style/layout/paint, frames, and how to turn flame charts into INP fixes.
- Long Tasks APIDetect main-thread tasks over 50ms with PerformanceObserver longtask entries and fix INP-killing work.
- Browser Rendering PipelineFrom bytes to pixels — parse, style, layout, paint, composite — and how your JS/CSS kicks each stage.
- BFCache Back Forward CacheHow the back/forward cache freezes pages for instant history nav, what blocks it, and how to restore state safely.