Перейти к содержимому

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 вызывается точно перед каждым frame
function 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 в rAF
const widths = [];
for (let i = 0; i < 100; i++) widths.push(el[i].offsetWidth); // все reads
requestAnimationFrame(() => {
for (let i = 0; i < 100; i++) el[i].style.width = widths[i] + 1 + 'px';
}); // один reflow в конце кадра
// Long Task блокирует rendering: разбить на chunks через setTimeout
function 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.