Every WebGL tutorial starts the same way: a requestAnimationFrame loop that draws forever.
For a game that is correct — something moves every frame. For a monitoring dashboard it means
the GPU redraws an unchanged scene sixty times a second for the whole shift, and every laptop
in the control room doubles as a heater.
The alternative is embarrassingly simple: draw when something changed. Data arrival marks the scene dirty. A resize marks it dirty. A pan, a zoom, a legend toggle — dirty. One scheduled frame coalesces however many changes landed since the last draw, renders once, and goes back to sleep. An unchanged scene costs nothing, because nothing runs.
The subtlety is that “changed” has to be cheap to detect and impossible to miss. Every input
funnels through one invalidate() — there is no second path to the canvas. Miss a call site
and the bug is obvious (the chart freezes until the next data tick), which is the failure
mode you want: loud, immediate, and pointing at its own cause. The reverse bug — drawing too
often — is silent and eats batteries for a year before anyone files it.
Animations still work; they just declare themselves. A camera tween marks the scene dirty for
its duration and stops. The loop exists while something is genuinely in motion and evaporates
after — which is what requestAnimationFrame was for before it became boilerplate.
On the instrumented build the numbers were blunt: idle draw calls went from sixty a second to zero, and the busiest five minutes of a shift drew fewer frames than the old build spent on an empty screen. Nobody in the control room noticed anything, which was the point.
