rAF vs setInterval dla pętli animacji

setInterval(loop, 16) wygląda jak 60fps, ale nim nie jest. Oto dlaczego requestAnimationFrame jest jedynym poprawnym narzędziem dla interaktywnych symulacji.

Każda symulacja na tej stronie używa pętli requestAnimationFrame (rAF). Powody wykraczają poza „bo to nowoczesny sposób" — istnieją konkretne, mierzalne różnice wpływające na poprawność, wydajność i doświadczenie użytkownika.

Co dokładnie jest nie tak z setInterval?

setInterval(fn, 16) wywołuje funkcję co 16 ms niezależnie od tego, czy przeglądarka jest gotowa do renderowania. Powoduje to trzy problemy:

Co daje requestAnimationFrame

Wywołania zwrotne rAF otrzymują parametr DOMHighResTimeStamp z (zazwyczaj) precyzją poniżej milisekundy. Wywołanie zwrotne uruchamia się z częstotliwością odświeżania ekranu — przeglądarka planuje je tuż przed kolejnym malowaniem, w synchronizacji z kompozytorem.

Poprawny wzorzec

let prevTime = performance.now();

function loop(time) {
  const dt = Math.min((time - prevTime) / 1000, 0.05); // sekundy, ograniczone do 50ms
  prevTime = time;

  update(dt);   // krok fizyki — mnożenie przyspieszeń przez dt, prędkości przez dt
  render();     // rysowanie

  requestAnimationFrame(loop);
}

requestAnimationFrame(loop); // start

Ograniczenie Math.min(..., 0.05) jest ważne: jeśli karta była ukryta i wróciła, dt może wynosić kilka sekund, powodując teleportację obiektów. Ograniczenie limituje maksymalny krok fizyki do 50 ms, by krótkie opóźnienie nie wysadziło symulacji.

Stały krok czasowy (dla deterministycznej fizyki)

Dla stabilnej fizyki (wykrywanie kolizji, dynamika sprężyn) zmienny dt może powodować niestabilność przy niskim FPS. Użyj akumulatora ze stałym krokiem czasowym:

const FIXED_DT = 1 / 60; // fizyka 60Hz
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);
}

Wykonuje to tyle kroków fizyki, ile potrzeba, by „nadgonić" czas rzeczywisty, przy czym każdy krok wynosi dokładnie 1/60 s. Renderowanie może wystąpić między krokami — użyj pozostałej frakcji accumulator / FIXED_DT, by interpolować stan renderowania dla płynnej wizualizacji nawet przy 120 Hz.

✅ requestAnimationFrame

  • Wyrównany z vsync, bez rozrywania obrazu
  • Automatycznie wstrzymuje się w ukrytych kartach
  • Dokładny znacznik czasu wysokiej rozdzielczości
  • Zoptymalizowane planowanie przeglądarki
  • Zużywa mniej CPU, gdy ukryty

❌ setInterval

  • Brak wyrównania z vsync
  • Działa w ukrytych kartach (drenaż baterii)
  • Dryf timera pod obciążeniem
  • Brak wbudowanego czasu delta
  • Może powodować „spiralę śmierci" przy nadganianiu

Zasada: nigdy nie używaj setInterval do animacji. Użyj requestAnimationFrame z akumulatorem czasu delta dla renderowania o zmiennej częstotliwości i dodaj wewnętrzną pętlę ze stałym krokiem czasowym dla stabilności fizyki.