setInterval(fn, 16)✕ not synced to paint
0
FPS
// fires on a timer, not before paint setInterval(() => { x += 2; draw(); }, 16);
requestAnimationFrame✓ synced to display
0
FPS
// runs right before each repaint function loop(t) { x += 2; draw(); requestAnimationFrame(loop); }
Add load and watch setInterval jank while rAF adapts.
Same code, different screens. Both balls move on every frame. The red ball adds a fixed 5px per frame; the green ball uses delta time. Drag the refresh rate up and watch the red ball speed up while green stays locked.
Refresh rate 60 Hz
❌ fixed 5px / frame0 px/s
OVERSHOOT →
✅ delta time (300px/sec)0 px/s
// ❌ fixed per frame — speed depends on Hz x += 5; // 60Hz→300px/s 144Hz→720px/s // ✅ delta time — same speed everywhere const dt = (t - last) / 1000; // seconds since last frame x += 300 * dt; // 300px/sec on ANY display
❌ No cleanupLeaks loops
0
loops still running after unmount
// ❌ loop outlives the component function loop() { update(); requestAnimationFrame(loop); } requestAnimationFrame(loop); // unmount → loop keeps running!
✅ cancelAnimationFrameClean
0
loops still running after unmount
// ✅ store id, cancel on cleanup let id; function loop() { update(); id = requestAnimationFrame(loop); } id = requestAnimationFrame(loop); // cleanup: cancelAnimationFrame(id);