Calci.inCalculate Today for a Better Tomorrow

Online Stopwatch

Accurate timing with laps

The guide

Stopwatch accuracy, and why browser timers drift

Why setInterval is the wrong way to build a timer, how background tabs are throttled, and what accuracy a browser stopwatch can actually deliver.

Last reviewed · 1,260 words

In short

  • A timer built by counting setInterval ticks drifts, because each tick fires late and the lateness accumulates.
  • The correct approach records a start timestamp and computes elapsed time on each frame, so drift cannot accumulate.
  • Background tabs are throttled to roughly one tick per second, and heavily throttled after five minutes.
  • performance.now() is monotonic and immune to clock changes; Date.now() is not, and a system clock sync can move it backwards.
  • Human reaction time is 200 to 250 milliseconds, which exceeds any timing error a well-built browser stopwatch introduces.

A stopwatch in a browser looks trivial and is a good example of a problem where the obvious implementation is wrong in a way that only shows up after several minutes.

Why counting ticks drifts

The intuitive approach is to run setInterval every 10 milliseconds and add 10 to a counter each time.

It drifts, and it drifts in one direction.

setInterval guarantees a minimum delay, not an exact one. The callback fires when the main thread is free, which is at least 10 ms later and frequently more. Each late tick adds its lateness to the accumulated total, and the errors only ever push the count behind real time.

Over an hour, a tick-counting timer can lose several seconds. Over a day it is minutes. Every such timer is slow, never fast, because a callback cannot fire early.

The correct approach

Record the start time once, and on every update compute the difference from now:

start = performance.now()
// on each frame:
elapsed = performance.now() - start

The displayed value is derived from the clock rather than accumulated, so a late frame produces a slightly jumpy display and no drift at all. The next frame is correct regardless of how late the previous one was.

Updates are driven by requestAnimationFrame, which fires once per screen refresh — usually 60 times a second — and pauses automatically when the tab is hidden. That pause does not matter, because the elapsed time is recomputed from the clock when the tab returns.

Which clock

Two are available and they behave differently.

Date.now() returns wall clock time in milliseconds since 1970. It is not monotonic: an NTP synchronisation, a manual clock change or a daylight saving transition can move it, including backwards. A stopwatch using it can show negative elapsed time.

performance.now() returns milliseconds since the page loaded, from a monotonic source that only moves forward. It is immune to clock adjustments and has sub-millisecond resolution in principle.

Use performance.now() for durations, always. Use Date.now() only for absolute timestamps.

One caveat: browsers deliberately reduce the resolution of performance.now() — typically to 0.1 ms, and coarser in some configurations — as a defence against timing attacks such as Spectre. That is far finer than any human timing need.

Background tab throttling

Browsers throttle timers in hidden tabs to save battery, and the throttling is aggressive.

StateTimer behaviour
Visible tabFull rate; requestAnimationFrame at screen refresh
Hidden tabTimers limited to roughly once per second
Hidden over 5 minutesThrottled to roughly once per minute
Heavily backgroundedMay be frozen entirely

For a correctly built stopwatch this changes only the display. The elapsed time is computed from the clock, so switching back shows the right value immediately, however long the tab was hidden.

For a tick-counting stopwatch it is catastrophic. Switch away for ten minutes and it counts ten seconds.

This is the clearest practical demonstration of why the two implementations are not equivalent, and it is easy to test: start a stopwatch, switch tabs for two minutes, and compare it against a phone.

What accuracy is achievable

A well-built browser stopwatch is accurate to within a few milliseconds over any duration, because it reads a monotonic clock rather than accumulating.

The display updates at the screen refresh rate, so the shown value can be up to about 16 ms behind on a 60 Hz screen. That is a rendering delay, not a measurement error — the underlying value is correct.

The limiting factor is not the software:

SourceTypical magnitude
Human reaction time200–250 ms
Display refresh8–16 ms
Input event latency5–20 ms
Clock resolution0.1 ms

Human reaction dominates by a factor of ten or more. Anyone starting and stopping a browser stopwatch by hand introduces far more error than the browser does, which is why hand timing in athletics is conventionally recorded to a tenth of a second while electronic timing goes to a thousandth.

For anything where the timing genuinely matters — a race result, a scientific measurement — the answer is hardware triggering, not a better stopwatch.

Lap and split

Two related measurements that are frequently confused.

Lap time is the duration of each segment individually. Four laps of 60, 62, 61 and 59 seconds.

Split time is the cumulative elapsed time at each mark. The same four laps give splits of 60, 122, 183 and 242 seconds.

Lap times answer "how fast was that section". Splits answer "am I on pace". Runners generally want splits during a race and laps afterwards.

A stopwatch should record both, since each is derivable from the other and having them presented separately saves arithmetic at exactly the moment nobody wants to do arithmetic.

Practical use

Interval training. Work and rest periods, where a countdown is usually better than a stopwatch because it does not require watching.

Cooking, where a countdown with an alarm is the right tool.

Billing time, where the requirement is a record rather than precision, and a running total across sessions matters more than the stopwatch itself.

Pacing a talk, where a large visible display and a discreet warning are what is actually needed.

Anything competitive should not use a browser stopwatch, for the reaction time reason above.

Keep the tab visible where the display matters. The value stays correct either way, and a throttled tab will not update the screen.

Building one correctly

For anyone implementing this, the pattern is short and the details are where it goes wrong.

State. Keep the start timestamp and an accumulated total for previous runs. Pausing adds the current run to the accumulated total and clears the start; resuming sets a new start.

Do not store the displayed value as state. It is derived, and storing it is what reintroduces drift.

Update with requestAnimationFrame, not setInterval. It matches the screen refresh, it pauses automatically when hidden, and it costs nothing to resume.

Format on display only. Converting milliseconds to hours, minutes, seconds and hundredths is presentation, and doing it in the state layer invites rounding errors to accumulate.

Handle the tab returning. Nothing special is needed if the value is computed from the clock — which is the point of building it that way.

Persist across reloads by storing the start timestamp rather than the elapsed time. A stopwatch that survives an accidental refresh is meaningfully better and costs one line.

Hundredths, and what a display should show

Showing more precision than the measurement supports is misleading, and stopwatch displays routinely do it.

Hundredths of a second is the conventional resolution and is already finer than a human can start or stop. It is worth showing because the last digits move visibly, which tells the user the timer is running.

Thousandths are noise for hand timing. The reaction error is two hundred times larger.

Above an hour, hundredths become clutter and the useful display is hours, minutes and seconds.

The convention that reads best is to show only the units in use — 12.45 rather than 00:00:12.45 — and to add each larger unit as it becomes non-zero. A stopwatch cluttered with leading zeros is harder to read at a glance, which is the one thing it exists to support.

What this tool assumes

  • Elapsed time is computed from a monotonic clock on each frame, not accumulated from tick counts, so it does not drift.
  • The display updates at the screen refresh rate and pauses when the tab is hidden; the underlying value continues correctly.
  • Accuracy is limited by your reaction time, not by the timer.
  • Nothing is transmitted. Timing happens entirely in your browser.
  • For results that matter competitively or scientifically, use equipment designed for it.

Sources

Frequently asked questions

Is a browser stopwatch accurate?

It is, if it reads the clock rather than counting timer ticks. A setInterval does not fire on schedule under load, and browsers throttle it heavily in background tabs, so a stopwatch built that way can lose minutes over an hour. This one measures elapsed time from the high-resolution clock, so the count stays right regardless of how often the callback runs.

Does it keep running if I switch tabs?

Yes. The display stops updating while the tab is throttled, but the elapsed time is recalculated from the clock the moment you return, so nothing is lost.

What is a lap time?

The split since the previous lap, as opposed to the running total. Both are shown, and the fastest split is highlighted.

Will I lose my time if I refresh?

Yes. The stopwatch keeps no stored state, so refreshing starts over.