Every rAF Loop Needs a Stop Condition
NEVER: Ship a `requestAnimationFrame` loop with no terminal condition. `const tick = () => { draw(); requestAnimationFrame(tick); }` never ends: once the value has converged and the user has moved on, it still wakes 60x/sec to compute a delta of zero, burning a core and measurable battery even while the element is visible and idle — the usual cause of an idle tab at 10-15% CPU. Give every loop two exits: stop re-scheduling on convergence (or gesture end / `AbortSignal`), and call `cancelAnimationFrame` on teardown so an unmount cannot orphan the loop. (Distinct from `animations-ibelick-pause-offscreen`, which stops work the user cannot see.)
A requestAnimationFrame loop that re-schedules itself unconditionally never ends, and burns a core for as long as the tab lives
const tick = () => {
if (settled()) return; // terminal condition
id = requestAnimationFrame(tick);
};
return () => cancelAnimationFrame(id); // teardownBad
Good
Why it matters
The shape is always the same: `const tick = () => { draw(); requestAnimationFrame(tick); }`. It is correct while the animation is running and wrong for every second after — the value has converged, the spring has settled, the user has moved on, and the loop is still waking up sixty times a second to compute a delta of zero. That is the usual explanation for an idle tab sitting at 10-15% CPU, and on a laptop it is measurable battery drain for no pixels.
Every loop needs two exits: a terminal condition (the value converged, the gesture ended, an AbortSignal fired) that stops re-scheduling, and a `cancelAnimationFrame` in the teardown so a component that unmounts mid-animation does not leave an orphan loop holding a reference to a dead tree. This is a different failure from animations-ibelick-pause-offscreen: that rule stops work the user cannot see, this one stops work nobody needs even when the element is in full view.