rAF проти setInterval для циклів анімації

setInterval(loop, 16) виглядає як 60 кадрів на секунду, та насправді таким не є. Ось чому requestAnimationFrame — єдиний правильний інструмент для інтерактивних симуляцій.

Кожна симуляція на цьому сайті використовує цикл на основі requestAnimationFrame (rAF). Причини виходять далеко за межі «це сучасний спосіб» — існують конкретні, вимірювані відмінності, які впливають на коректність, продуктивність та користувацький досвід.

Що насправді не так із setInterval?

setInterval(fn, 16) спрацьовує кожні 16 мс незалежно від того, чи готовий браузер до рендерингу. Це створює три проблеми:

Що дає requestAnimationFrame

Колбеки rAF отримують параметр DOMHighResTimeStamp із точністю до частки мілісекунди (як правило). Колбек спрацьовує з частотою оновлення дисплея — браузер планує його якраз перед наступним малюванням, синхронно з компоузером.

Правильний патерн

let prevTime = performance.now();

function loop(time) {
  const dt = Math.min((time - prevTime) / 1000, 0.05); // секунди, обмежено 50 мс
  prevTime = time;

  update(dt);   // крок фізики — множимо прискорення на dt, швидкості на dt
  render();     // малюємо кадр

  requestAnimationFrame(loop);
}

requestAnimationFrame(loop); // старт

Обмеження Math.min(..., 0.05) важливе: якщо вкладка була прихована, а потім знову стала активною, dt міг би дорівнювати кільком секундам, через що об'єкти «телепортувалися» б. Обмеження встановлює максимальний крок фізики на рівні 50 мс, щоб короткий лаг не «підірвав» симуляцію.

Фіксований крок часу (для детермінованої фізики)

Для стабільної фізики (виявлення зіткнень, динаміка пружин) змінний dt може призводити до нестабільності при низькій частоті кадрів. Використовуйте акумулятор із фіксованим кроком часу:

const FIXED_DT = 1 / 60; // фізика на 60 Гц
let accumulator = 0;

function loop(time) {
  const frameTime = Math.min((time - prevTime) / 1000, 0.1);
  prevTime = time;
  accumulator += frameTime;

  while (accumulator >= FIXED_DT) {
    physicsStep(FIXED_DT);
    accumulator -= FIXED_DT;
  }

  render();
  requestAnimationFrame(loop);
}

Це виконує стільки кроків фізики, скільки потрібно, щоб «наздогнати» реальний час, при цьому кожен крок завжди дорівнює рівно 1/60 с. Рендеринг може відбуватися між кроками — використовуйте залишкову частку accumulator / FIXED_DT для інтерполяції стану рендерингу заради плавної картинки навіть при 120 Гц.

✅ requestAnimationFrame

  • Вирівняно за vsync, без розривів кадру
  • Автоматично призупиняється у прихованих вкладках
  • Точна мітка часу високої роздільної здатності
  • Планування, оптимізоване браузером
  • Менше навантаження на процесор у фоні

❌ setInterval

  • Не вирівняно за vsync
  • Працює у прихованих вкладках (розряджає батарею)
  • Дрейф таймера під навантаженням
  • Немає вбудованого дельта-часу
  • Може викликати «спіраль смерті» надолуження

Правило: Ніколи не використовуйте setInterval для анімації. Використовуйте requestAnimationFrame з акумулятором дельта-часу для рендерингу зі змінною частотою та додавайте внутрішній цикл із фіксованим кроком часу для стабільності фізики.