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:
- A loop runs roughly every 25 milliseconds — frequently, but its own imprecision no longer matters.
- Each time it wakes up, it schedules every click whose exact time falls within the next ~100–150 milliseconds, using
osc.start(exactAudioClockTime). - Because the *audio hardware itself* fires the click at that exact sample-accurate time — not the JavaScript timer — the click's timing is immune to the timer's own jitter. The timer only has to be roughly on time; the clock reference it reads from is exact.
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.