Skip to content
Back to the blog

Why your online metronome drifts, and how ours doesn't

A browser tab is a bad place to keep time, unless you keep it on the audio clock. What goes wrong in a metronome built on setInterval, and what we do instead.

PJ Doland5 min read
metronomeengineering

You have probably used a metronome in a browser tab that was not quite right. Not obviously wrong, which would be easy to dismiss. Wrong in the way that makes you play badly and blame yourself: the click sitting a hair late against your own playing, the pulse loosening while you are counting a long note, the whole thing lurching after you switched tabs to look at the score.

The cause is almost always the same, and it is worth understanding, because it tells you which metronomes to trust.

Two clocks

There are two clocks available to a page in your browser and they are not equally good at this job.

The first is the main thread's timer: setTimeout and setInterval. This is the clock most metronomes are built on. You ask for a callback every 500 milliseconds and you play a click each time.

The second is the audio clock. When a page opens an AudioContext, it gets currentTime, a monotonic clock driven by the audio hardware, and it can hand a sound to that hardware with an instruction that amounts to "play this at exactly 12.483 seconds". The sound is then scheduled sample by sample, below the level of anything JavaScript is doing.

A metronome on the first clock is a metronome made of intentions. A metronome on the second clock is a metronome made of appointments.

What a JavaScript timer actually promises

Very little. setInterval(fn, 500) promises that your function will not run sooner than 500 milliseconds from now. It says nothing about how much later than that it will run.

In practice it runs a few milliseconds late, and the amount varies with whatever else the page is doing: laying out an image, running a script, garbage collecting. A few milliseconds is not much. The problem is that a naive metronome starts its next interval from the moment the last one fired, so the errors accumulate. Late by three, late by two, late by five, and forty beats later the click is a noticeable distance from where it should be. That accumulation is what people mean by drift.

Then you switch tabs. Browsers throttle background timers hard, often to once per second, sometimes further, and requestAnimationFrame stops entirely when the tab is hidden. A metronome built on those either stops, slows down, or comes back and fires a burst of the beats it owed you.

What we do instead

The metronome here never asks a timer when to make a sound. It uses the timer only to ask, frequently, whether there is any scheduling work to do.

  • A plain interval wakes up every 25 milliseconds.
  • On each wake-up it looks 100 milliseconds ahead on the audio clock and schedules every beat that falls inside that window, each with an exact start time.
  • Beat times are computed arithmetically. Beat n is the start time plus n times 60 / bpm seconds. They are not measured from when the previous beat happened, so a late wake-up cannot push the grid.

This is the standard design for audio in a browser, usually filed under "A Tale of Two Clocks". The timer can be ten milliseconds late every time and nothing changes: it is only there to keep the queue topped up, and it has a 100 millisecond cushion to do it in. The clicks themselves are already appointments on the audio hardware.

The result is a beat grid that does not accumulate error. At 120 beats per minute the thousandth click is exactly 500 seconds after the first, because that is arithmetic, not the sum of a thousand timer callbacks.

The tab-switch case

If you background the tab or your laptop sleeps, the 25 millisecond timer gets throttled and the audio clock keeps running. When the tab comes back, the scheduler may find that the next beat it planned is now in the past.

It does not play those beats. It fast-forwards the beat counter past everything it missed and picks up on the original grid. You get silence while you were away and the correct pulse when you return, rather than a machine-gun burst of apologies.

The needle is a follower, not the beat

The animated needle sweeps between plus and minus 45 degrees, reaching an extreme exactly on each beat. It is drawn with requestAnimationFrame and it is evaluated against the same audio clock the clicks are scheduled on.

It is display only. If frames drop because your machine is busy, the needle stutters and the clicks are untouched. This is worth stating plainly because the earlier version of this metronome did it the other way round, with a simulated pendulum generating the clicks, and it was subtly, maddeningly uneven. Animation must never be the source of a beat.

What this does not fix

Honesty about the limits, because a metronome that claims too much is worse than one that claims nothing.

The audio clock schedules the click accurately; it does not control your speakers, your Bluetooth headphones, or your operating system's audio buffer. Bluetooth in particular adds latency, and while that latency is usually constant, and constant latency is inaudible when the click is the only thing you are hearing, it is real. If you are recording yourself against the click, use wired output.

Nothing in a browser can survive the tab being suspended by the operating system, which phones do aggressively when the screen goes off. Keep the screen on.

And it will not stop you rushing. It will only make it obvious that you are.

What it does not have

No tap tempo, no subdivisions, no accented downbeat, and it does not remember your tempo between visits. The dial runs from 40 to 208 with presets at Largo 50, Andante 92, Allegro 144 and Presto 184, there is a volume control, and the spacebar starts and stops it. That is the whole feature set. Those gaps are on the list, and until they are built the honest thing is to say so.

The timing, though, is not on the list, because it is done. The scheduler has a test suite that pins it down: beats land at exactly 60 / bpm spacing on the audio clock, every beat is scheduled once and always ahead of its play time, a tempo change applies to later beats without restarting anything, and a simulated background-tab clock jump produces a resync rather than a burst.

It is free and it needs no account: /tools/metronome.

Try it on the piece you are working on.

Free, with no billing system behind it to find later.