What a metronome taught me about time in JavaScript
Metrono is an app for logging music practice, and it has a built-in metronome. I prototyped that metronome in the browser first, with a timer that fired on every beat. At 60 BPM it sounded fine. At 180 BPM, with the screen busy, you could hear it stumble.
Two clocks, one job each
The JavaScript timer is a clock for the interface. It runs late whenever the main thread is busy. The audio clock is the one the speakers follow, and it does not care what the screen is doing.
So the timer no longer plays clicks. It wakes up every 25 ms, looks 100 ms ahead and books any beat that falls in that window on the audio clock.

const LOOKAHEAD = 0.1; // seconds booked ahead
const TICK_MS = 25; // how often we check
let nextBeat = ctx.currentTime;
function scheduler(bpm: number) {
const step = 60 / bpm;
while (nextBeat < ctx.currentTime + LOOKAHEAD) {
playClick(nextBeat);
nextBeat += step;
}
}
setInterval(() => scheduler(tempo), TICK_MS);The interface follows, it does not lead
The flashing beat light reads the same schedule and draws on the next frame. If a frame drops, the light is a little late. The sound is not.
- Keep timing out of the UI thread wherever the platform allows it.
- Schedule in small windows, so tempo changes apply within a beat.
- Test at the extremes: 30 BPM, 240 BPM, low power mode.
People forgive a late animation. Musicians never forgive a late click.
The same rule shows up in design work more often than you would think: find the clock the user actually trusts, and make everything else follow it.
Building something and need a designer who can also ship it? Get in touch →