Why Your Metronome Should Never Use setInterval (And How Ours Does)

The obvious way to build a metronome is setInterval(click, 60000/bpm). It works — for about thirty seconds. Then it starts to drift, and on a long practice session the drift becomes audible. Here's why, and what a metronome that actually holds tempo has to do instead.

The problem with JavaScript timers

setInterval and setTimeout are best-effort: the browser guarantees your callback won't fire before the requested delay, but makes no promise about exactly when after. If the tab is backgrounded, the OS is busy, garbage collection kicks in, or a dozen other timers are queued ahead of yours, your callback fires late — sometimes by a few milliseconds, sometimes by much more. Each late firing is a small timing error, and a metronome built this way accumulates those errors beat after beat. A tempo that's supposed to be locked at 120 BPM can wander by several BPM over a few minutes of continuous practice, exactly when a click track most needs to be trustworthy.

The lookahead scheduler pattern

Web Audio exposes its own high-precision clock, audioCtx.currentTime, and every audio node accepts an exact start time on that clock rather than "play now." The fix is to stop asking a timer to fire at each beat, and instead use a (still-imprecise) timer only to periodically check the clock and schedule any upcoming beats slightly ahead of when they're due:

This is the same technique described in Chris Wilson's well-known "A Tale of Two Clocks" article, and it's become the standard approach for any web app that needs a drum machine, sequencer, or metronome that actually holds time. perfecttune.net's Metronome uses exactly this pattern: a lookahead of about 100ms and a scheduler tick of 25ms, with every click's start time computed directly from tempo and time signature rather than counted one interval at a time.

Making the visuals match

It's not enough for the audio to be accurate if the pendulum on screen is animated by a separate, disconnected timer — you'd see drift even if you couldn't hear it. So the pendulum's position each frame is computed directly from the same schedule: which beat just played, which beat is coming next, and how far between those two real audio-clock timestamps the current moment falls. What you see is a direct readout of what's actually scheduled to play, not a decorative approximation of it.

← Back to perfecttune.net