Browser Event Loop & Rendering
Браузерный event loop отличается от Node.js наличием rendering pipeline между итерациями (style → layout → paint → composite), и понимание момента рендера критично для оптимизации анимаций и диагностики Layout Thrashing.
Browser Event Loop — одна итерация ┌────────────────────────────────────────────────────────┐ │ │ │ [Macrotask Queue] → взять ОДИН task │ │ setTimeout, setInterval, I/O, click/keydown, │ │ MessageChannel, script parse │ │ │ │ │ ▼ │ │ ┌───────────────┐ │ │ │ Call Stack │ выполнить task │ │ └───────┬───────┘ │ │ │ │ │ ▼ │ │ [Microtask Queue] → дренировать ВСЕ │ │ Promise .then / .catch / .finally │ │ queueMicrotask, MutationObserver │ │ (добавленные в процессе тоже обрабатываются) │ │ │ │ │ ▼ │ │ Rendering Pipeline (если браузер решил обновить) │ │ ┌──────────────────────────────────────────────┐ │ │ │ requestAnimationFrame callbacks │ │ │ │ ↓ │ │ │ │ Style recalculation │ │ │ │ ↓ │ │ │ │ Layout (reflow) │ │ │ │ ↓ │ │ │ │ Paint → Composite (GPU layers) │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ [requestIdleCallback] (если осталось время) │ │ │ │ │ └──────────→ следующая итерация │ └────────────────────────────────────────────────────────┘
// rAF vs setTimeout для анимацийlet pos = 0;
// BAD: setTimeout не синхронизирован с refresh rate (60/120Hz)function animateBad() { pos += 1; el.style.left = pos + 'px'; setTimeout(animateBad, 16); // ~60fps, но может пропускать кадры}
// GOOD: rAF вызывается точно перед каждым framefunction animateGood(timestamp) { pos += 1; el.style.left = pos + 'px'; requestAnimationFrame(animateGood);}requestAnimationFrame(animateGood);
// rAF автоматически паузится во вкладках не в фокусе — экономит батарею// Layout Thrashing: чередование read/write → forced reflow каждый раз
// BAD: 100 reflow-овfor (let i = 0; i < 100; i++) { el[i].style.width = el[i].offsetWidth + 1 + 'px'; // read → write → reflow}
// GOOD: батчинг — сначала все reads, потом все writes в rAFconst widths = [];for (let i = 0; i < 100; i++) widths.push(el[i].offsetWidth); // все readsrequestAnimationFrame(() => { for (let i = 0; i < 100; i++) el[i].style.width = widths[i] + 1 + 'px';}); // один reflow в конце кадра// Long Task блокирует rendering: разбить на chunks через setTimeoutfunction chunkedWork(items, onDone) { const CHUNK = 50; let i = 0; function process() { const end = Math.min(i + CHUNK, items.length); while (i < end) doWork(items[i++]); if (i < items.length) { setTimeout(process, 0); // yield → браузер может перерисовать } else { onDone(); } } process();}// Или scheduler.postTask() (Chrome 94+) с приоритетами// scheduler.postTask(fn, { priority: 'background' })Итог: Рендер происходит между итерациями event loop — после дренирования microtask queue и только если браузер решил обновить экран; requestAnimationFrame — единственный надёжный hook перед paint.