Frame loop
帧循环 · frame-loopUse one loop for continuous motion, measure dt in seconds, and handle stalls and background returns.
Adding a fixed number of pixels per frame changes speed with refresh rate. Real elapsed time fixes that, but advancing all ten seconds after a background pause teleports the element. The loop needs a frame-time clamp and a clear policy for hidden tabs.
Formula
rawDt = (now − previous) / 1000 dt = min(rawDt, maxDt) position += speed × dt if hidden: pause; on return: reset previous
Parameters
| This page | Mature libraries | |
|---|---|---|
| maxDt | 40 ms | Motion maxElapsed = 40 ms |
| speed | 110 px/s | — |
| visibility | pause while hidden | — |
Doing it well
- Update continuous values in one rAF so position and velocity share the same frame time.www-motion/rewrite/lib/motion-kit.js:290–306
- Clamp unusually large dt. Motion limits elapsed time to 40 ms; our case runtime allows at most three 60 Hz steps per frame.motion/packages/motion-dom/src/frameloop/batcher.ts:6,48; www-motion/rewrite/lib/case-runtime.js:22–27
- Stop when hidden and reset the previous time on resume; do not count background time as one frame.www-motion/rewrite/lib/case-runtime.js:28–40
In our cases
Across frame-loop, dt-clamp, and visibility-gate, 64 distinct studies are covered; 19 clamp frame time and 53 gate on visibility. rewrite/lib/case-runtime.js already passes a capped step and stops animations when the tab is hidden.
49 / 69 cases · integrate per frame (no dt)From Codex per-case quality flags (keyword-clustered, approximate)
Used in 64 cases
+46 more
Reference implementation · basics.js
/**
* The single writer: one rAF loop owns every continuous value.
* dt is in seconds and clamped (a backgrounded tab returns with a 10 s frame);
* the loop pauses while the page is hidden.
*/
export function frameLoop(update, { maxDt = 0.04 } = {}) {
let raf = 0, last = 0, running = false;
const frame = now => {
const dt = last ? Math.min((now - last) / 1000, maxDt) : 1 / 60;
last = now; update(dt, now / 1000);
raf = requestAnimationFrame(frame);
};
const start = () => { if (!running) { running = true; last = 0; raf = requestAnimationFrame(frame); } };
const stop = () => { running = false; cancelAnimationFrame(raf); };
const onVis = () => (document.hidden ? stop() : start());
document.addEventListener('visibilitychange', onVis);
if (!document.hidden) start();
return { start, stop, destroy() { stop(); document.removeEventListener('visibilitychange', onVis); } };
}