Функція requestAnimationFrame()
requestAnimationFrame(callback), це вбудований API браузера, який планує виконання функції callback перед наступним кадром відмальовування, тобто перед черговим перемальовуванням екрана. Браузер сам вирішує, коли саме настане цей момент, і завдяки цьому анімація йде в такт із частотою оновлення дисплея.
Теорія
TL;DR
requestAnimationFrame(fn)викликаєfnбезпосередньо перед наступним кадром відмальовування.- Браузер малює приблизно 60 кадрів на секунду, тобто кадр кожні 16.6 мс, і підлаштовує виклики під реальну частоту екрана.
- У колбек передається
timestamp, час початку кадру в мілісекундах за шкалоюperformance.now(). - Виклик повертає ID кадру, який скасовується через
cancelAnimationFrame(id). - Повторний виклик усередині колбека утворює цикл анімації, кадр за кадром.
- На неактивній вкладці браузер призупиняє виклики, тому анімація не витрачає батарею й CPU.
Швидкий приклад
function animate(time) {
console.log('Frame at', time, 'ms');
requestAnimationFrame(animate); // request the next frame
}
requestAnimationFrame(animate);Що тут відбувається:
animateвикликається перед кожним кадром відмальовування;time, це timestamp у мілісекундах, який показує, коли саме почалося відмальовування кадру (заperformance.now()).
Як це працює під капотом
- Ви викликаєте
requestAnimationFrame(fn). - Функція
fnзберігається браузером у списку колбеків поточного кадру. - Коли браузер готовий малювати новий кадр (зазвичай приблизно через 16 мс), він перед перемальовуванням викликає всі такі функції.
- Після виклику колбеків браузер перемальовує екран.
Якщо всередині колбека знову викликати requestAnimationFrame(), анімація піде в циклі, кадр за кадром. Важливо, що це окрема черга візуальних оновлень: вона обробляється перед рендером, а не разом зі звичайними макротасками.
Плавне переміщення елемента
const box = document.querySelector('.box');
let start = null;
function move(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
box.style.transform = `translateX(${progress / 5}px)`;
if (progress < 2000) { // 2 seconds
requestAnimationFrame(move);
}
}
requestAnimationFrame(move);Тут елемент плавно рухається протягом двох секунд. Прогрес рахується не від кількості кадрів, а від різниці timestamp, тому на швидкому й на повільному пристрої анімація триватиме однаковий час. Браузер сам оптимізує виклики: якщо вкладка не видима, requestAnimationFrame призупиняється, щоб не витрачати ресурси.
Відмінності від setTimeout і setInterval
| Критерій | setTimeout / setInterval | requestAnimationFrame |
|---|---|---|
| Частота | Фіксована (наприклад, кожні 16 мс) | Синхронізується з частотою оновлення екрана |
| Енергоспоживання | Не призупиняються у прихованих вкладках | Автоматично зупиняється, якщо вкладка прихована |
| Плавність | Може «рватися» під навантаженням | Максимально плавне відмальовування |
| Використання | Таймери, фонові задачі | Анімації, плавні оновлення UI |
| Передає timestamp | Ні | Так, час кадру за performance.now() |
Той самий рух, написаний двома способами:
function moveWithTimeout() {
box.style.left = `${parseInt(box.style.left || 0) + 1}px`;
setTimeout(moveWithTimeout, 16);
}
function moveWithRaf() {
box.style.left = `${parseInt(box.style.left || 0) + 1}px`;
requestAnimationFrame(moveWithRaf);
}setTimeout не враховує навантаження на браузер і може «проскакувати» кадри: 16 мс, це лише мінімальна затримка, а не гарантія. requestAnimationFrame підлаштовується під частоту дисплея, зокрема під 120 Гц, і завжди дає максимально плавний результат.
Скасування кадру
Якщо запланований кадр більше не потрібен, його скасовують за ID:
const id = requestAnimationFrame(step);
cancelAnimationFrame(id);Це обов'язково робити, коли компонент розмонтовується або анімація зупиняється достроково, інакше цикл продовжить працювати й триматиме посилання на вже непотрібні вузли DOM.
Підсумок
| Властивість | Опис |
|---|---|
| Що робить | Запускає колбек перед наступним кадром відмальовування |
| Коли викликається | Коли браузер готовий намалювати новий кадр (приблизно 60 FPS) |
| Аргумент функції | timestamp, час початку кадру |
| Повертає | ID кадру (для cancelAnimationFrame) |
| Зупиняється автоматично | Так, якщо вкладка неактивна |
| Тип задачі | Окрема черга візуальних оновлень, до рендера |
| Ідеально підходить для | Анімацій, плавних трансформацій, прогрес-барів |
Типові помилки
- Анімувати через
setInterval(fn, 16). Таймер не синхронізований з екраном, кадри «пливуть», а на прихованій вкладці робота триває дарма. - Рахувати прогрес у кадрах, а не в часі. Кількість кадрів залежить від пристрою, тому анімація на 120 Гц закінчиться вдвічі швидше. Рахуйте від
timestamp. - Не скасовувати кадр. Без
cancelAnimationFrame()цикл продовжує працювати після того, як елемент зник, і тримає його в пам'яті. - Читати й писати стилі впереміш. Читання
offsetWidthпісля запису стилю всередині колбека викликає примусовий reflow і вбиває плавність. Спочатку читайте, потім пишіть. - Класти в колбек важкі обчислення. Якщо кадр не вклався в 16 мс, браузер його пропустить. Важку роботу виносьте у Worker або розбивайте між кадрами.
- Чекати на нього поза браузером. Це DOM API, у чистому Node.js його немає, там для схожих задач використовують таймери.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.