Skip to main content

requestAnimationFrame та requestIdleCallback в JavaScript

requestAnimationFrame планує виклик callback-функції прямо перед тим, як браузер перемалює наступний кадр. requestIdleCallback відкладає роботу до вільного часу, після того як завершено обробку вводу та рендеринг.

Теорія

TL;DR

  • RAF спрацьовує ~60 разів на секунду, синхронізовано з частотою оновлення дисплею (vsync). RIC спрацьовує нерегулярно, тільки коли браузеру більше нічим зайнятись.
  • Аналогія: RAF - це художник, який каже "пензель готовий?" перед кожним мазком. RIC - прибиральник, який прибирає тільки після того, як художник закінчив всю кімнату.
  • Головна різниця: RAF прив'язаний до циклу перемалювання (термінова візуалізація); RIC чекає після обробки вводу та layout (фонові задачі).
  • Анімація або оновлення UI? Використовуй RAF. Аналітика, логування або prefetch? RIC.
  • RIC не підтримується в Safari, тому завжди додавай { timeout: ms } як запасний варіант.

Короткий приклад

js
const box = document.querySelector('#box'); // RAF: спрацьовує перед кожним перемалюванням, ~60fps function animate(time) { box.style.transform = `translateX(${time * 0.05}px)`; requestAnimationFrame(animate); // плануємо наступний кадр } requestAnimationFrame(animate); // RIC: спрацьовує тільки під час вільних слотів function doIdleWork(deadline) { while (deadline.timeRemaining() > 0) { console.log('фонова задача'); // поважає доступний бюджет часу } requestIdleCallback(doIdleWork, { timeout: 200 }); // повторити, з fallback по таймауту } requestIdleCallback(doIdleWork, { timeout: 200 });

RAF передає у callback мітку часу performance.now() з високою точністю. RIC передає об'єкт deadline, щоб код міг перевірити, скільки вільного часу ще залишилось перед тим, як браузер забере потік назад.

Ключова різниця

Колбеки RAF запускаються під час фази "begin frame" в браузері, до перерахунку стилів, layout, paint і compositing. Вони синхронізовані з vsync-сигналом від GPU, зазвичай з інтервалом 16.7мс на 60Hz дисплеї. Колбеки RIC потрапляють у планувальник задач браузера після всього іншого і виконуються тільки в залишковому часовому слоті після повного рендерингу кадру. Цей слот зазвичай 4-10мс, а на завантаженій сторінці може бути і нульовим.

Коли що використовувати

  • DOM-анімації, canvas або все, що має залишатись плавним на 60fps: RAF.
  • Реакція на scroll чи resize, якщо зачіпається layout: RAF.
  • Аналітика та telemetry після гідратації: RIC.
  • Фонові черги синхронізації або ненав'язливий prefetch: RIC.
  • Вкладка згорнута: RAF автоматично призупиняється, RIC теж сповільнюється.
  • Мобільні пристрої або режим економії батареї: RIC з { timeout: 500 }, щоб callback точно не пропустили.

Таблиця порівняння

ПараметрrequestAnimationFramerequestIdleCallback
ТригерПеред наступним перемалюванням (~16мс на 60Hz)Після задач кадру (змінний вільний слот)
ПріоритетВисокий (візуальні оновлення)Низький (фон)
Параметри callbackcallback(timestamp)callback(deadline, options)
СкасуванняcancelAnimationFrame(id)cancelIdleCallback(id)
Підтримка браузерівВсі сучасні, IE10+Chrome/Edge 60+, Firefox 87+. Safari не підтримує.
Запасний варіантНемає{ timeout: ms } запустить callback після затримки
Типове застосуванняRender loop Three.js, частинки Phaser.jsАналітика Vercel, черги Workbox

Як браузер це обробляє

У Chromium колбеки RAF ставляться в чергу на головному потоці й запускаються під час події "begin frame" перед растеризацією, синхронно з vsync GPU. Тому вони залишаються плавними і на 120Hz: браузер просто викликає їх удвічі частіше. RIC управляється через IdleTaskController, який вимірює час, що залишився після стилів, layout і compositing, та виділяє залишок через deadline.timeRemaining(). Якщо сторінка ніколи не простоює (активна реклама, ігровий цикл), колбеки RIC можуть взагалі не запуститись без параметра timeout.

Типові помилки

Помилка: setInterval всередині RAF-циклу

js
// Неправильно: дві окремі черги, призводить до пропущених кадрів function badAnimate() { setInterval(() => { box.style.left = x++ + 'px'; }, 16); requestAnimationFrame(badAnimate); }

Інтервали виконуються в окремій черзі таймерів і не синхронізовані з vsync. Отримуєш подвійне планування і пропущені кадри. Використовуй тільки рекурсивний RAF.

Помилка: ігнорування deadline.timeRemaining() в RIC

js
// Неправильно: може виконуватись 100мс, блокує наступний paint requestIdleCallback(() => { heavyComputation(); }); // Правильно: розбиваємо роботу на частини requestIdleCallback((deadline) => { while (deadline.timeRemaining() > 1 && tasks.length) { processOneTask(tasks.shift()); } });

Якщо перевищити вільний слот, наступна реакція на ввід або перемалювання відкладаються. Розбивай роботу на частини, що вкладаються в доступний бюджет.

Помилка: немає скасування при знищенні компонента

js
// Неправильно: id загублено, цикл не зупиняється useEffect(() => { requestAnimationFrame(animate); }, []); // Правильно: зберігаємо id, скасовуємо в cleanup useEffect(() => { const id = requestAnimationFrame(animate); return () => cancelAnimationFrame(id); }, []);

"Привид" RAF-циклу продовжує з'їдати CPU після того, як компонент видалено з DOM. Завжди зберігай id і скасовуй у функції cleanup.

Помилка: RIC без timeout на мобільних

Режим економії батареї та фонові вкладки можуть ніколи не дати вільного вікна. { timeout: 500 } вказує браузеру запустити callback через 500мс у будь-якому випадку. Без цього аналітичний код може зависнути на мобільних непомітно.

Де зустрічається в реальних проєктах

  • Three.js: renderer.setAnimationLoop - тонка обгортка над RAF.
  • React Reanimated: withSpring і драйвери жестів використовують RAF-цикли для покадрових оновлень.
  • Preact Signals: RAF групує DOM-коміти, щоб уникнути кількох перемалювань за один тік.
  • Vercel Analytics: RIC відкладає пінги pageview до завершення гідратації.
  • Workbox (PWA-бібліотека від Google): RIC керує чергами фонової синхронізації між запитами.

Питання на співбесіді

Q: Навіщо RAF передає timestamp у callback?
A: Це значення performance.now() з високою точністю. Воно дозволяє вирахувати delta = time - lastTime і зробити анімацію незалежною від частоти кадрів.

Q: Що відбувається з RAF, коли вкладка згорнута?
A: Браузер призупиняє або сповільнює його до приблизно 1fps, економлячи CPU і батарею. Звичайний setTimeout продовжував би виконуватись з повною частотою.

Q: Що таке об'єкт deadline у RIC?
A: Він має два члени: timeRemaining() повертає скільки мілісекунд залишилось у поточному вільному слоті, а didTimeout дорівнює true, якщо параметр timeout примусив запуск callback до появи вільного вікна.

Q: Як зробити polyfill для RIC в Safari?
A: Найпростіший варіант - обгорнути setTimeout з коротким затриманням (0-1мс). Точніший перевіряє performance.now() відносно останнього відомого зайнятого часу і відкладає відповідно.

Q: Senior-питання: на 120Hz дисплеї з важким JS, як RAF взаємодіє з compositor thread?
A: Колбеки RAF виконуються на головному потоці до стилів і layout. Але якщо застосувати will-change: transform або transform3d до елемента, браузер переносить його на compositor layer. Анімації на цьому шарі обробляються поза головним потоком, тому залишаються плавними навіть коли JS блокує. Відповідь рівня senior: "переноси на compositor layer через will-change, щоб малювання обійшло вузьке місце головного потоку."

Приклади

Плавна анімація лічильника в React

js
import { useRef, useEffect } from 'react'; function AnimatedCounter({ target }) { const countRef = useRef(0); useEffect(() => { let rafId; const animate = (time) => { // Плавно наближаємось до target: 5% відстані за кожен кадр countRef.current += (target - countRef.current) * 0.05; document.getElementById('counter').textContent = Math.round(countRef.current); if (Math.abs(target - countRef.current) > 0.5) { rafId = requestAnimationFrame(animate); } }; rafId = requestAnimationFrame(animate); return () => cancelAnimationFrame(rafId); // скасовуємо при знищенні }, [target]); return <span id="counter">0</span>; }

Кожен RAF-виклик наближає лічильник на 5% до цільового значення. Анімація природно сповільнюється без CSS-переходів. Скасування при unmount запобігає "привидному" циклу після видалення компонента з DOM.

Групування аналітики через RIC з timeout

js
const analyticsQueue = []; function flushAnalytics(deadline) { // Обробляємо події тільки поки є вільний час while (deadline.timeRemaining() > 1 && analyticsQueue.length > 0) { const event = analyticsQueue.shift(); sendToServer(event); // неблокуючий fetch } // Якщо черга не вичерпана, плануємо наступний вільний слот if (analyticsQueue.length > 0) { requestIdleCallback(flushAnalytics, { timeout: 300 }); } } function trackEvent(name, data) { analyticsQueue.push({ name, data, ts: performance.now() }); requestIdleCallback(flushAnalytics, { timeout: 300 }); }

{ timeout: 300 } гарантує очищення черги протягом 300мс навіть якщо сторінка залишається завантаженою. Я бачив цей підхід у production-дашбордах, де аналітика мовчки зникала на мобільних до того, як додали timeout. Без нього RIC-колбеки можуть накопичуватись нескінченно на важких сторінках.

Ігровий цикл з delta-time (стиль Three.js)

js
const clock = { last: 0 }; function gameLoop(time) { const delta = time - clock.last; // мілісекунди з останнього кадру clock.last = time; updatePhysics(delta); // рухаємо об'єкти пропорційно до delta updateCamera(delta); renderer.render(scene, camera); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop);

Використання delta робить рух незалежним від частоти кадрів. На 60fps delta дорівнює ~16мс, на 120fps - ~8мс. Об'єкт проходить однакову відстань за секунду в обох випадках, що важливо коли у користувачів різне залізо.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?