The Devs Tools

Developer's Guide to Chronometer: Best Practices and Examples

August 18, 2026 · The Devs Tools Team

A chronometer, or stopwatch, is a deceptively simple tool built on a subtly tricky foundation: measuring elapsed wall-clock time accurately inside a web browser. Naive implementations track elapsed time by incrementing a counter on a setInterval tick, which drifts under load because JavaScript timers are not guaranteed to fire at exact intervals — the event loop can be delayed by rendering, garbage collection, or a busy tab in the background. A more reliable approach records a start timestamp using a monotonic clock source and computes elapsed time as now - start on every render or tick, so the displayed value stays accurate even if individual ticks are late or dropped. The performance.now() API is the standard tool for this in browsers: unlike Date.now(), it returns a high-resolution timestamp (sub-millisecond precision) that isn't affected by system clock adjustments, such as NTP synchronization or the user changing their system time mid-session. Split (lap) timing extends this by recording a snapshot of the elapsed value at each lap boundary without resetting the underlying start time, letting you review individual segment durations after the fact — useful for anything from tracking build step durations to comparing interval workout splits. Pausing correctly requires storing the accumulated elapsed time separately from the running timer, so that resuming adds a new start offset instead of restarting from zero.

[!TIP] Need to time something precisely right now? Try our free, local Chronometer to start, pause, and log lap splits with high-resolution accuracy, completely offline.


Why setInterval Alone Isn't Enough

A common but flawed stopwatch implementation looks like this:

let elapsed = 0;
setInterval(() => {
  elapsed += 10; // assumes exactly 10ms passed — often wrong
  render(elapsed);
}, 10);

The problem: setInterval only guarantees a minimum delay, not an exact one. Under any CPU contention, the actual gap between ticks can be 15ms, 30ms, or more, so the displayed time silently drifts behind reality. The fix is to always compute elapsed time from a fixed reference point rather than accumulating per-tick deltas:

const start = performance.now();
function render() {
  const elapsed = performance.now() - start;
  update(elapsed);
  requestAnimationFrame(render);
}

This way, the displayed value is always correct regardless of how many frames were skipped in between.

Handling Pause and Resume

Pausing needs to freeze the display without losing the running total:

let accumulated = 0;
let start = null;

function play() {
  start = performance.now();
}
function pause() {
  accumulated += performance.now() - start;
  start = null;
}
function getElapsed() {
  return start === null ? accumulated : accumulated + (performance.now() - start);
}

Recording Lap Splits

A lap doesn't reset the timer — it just captures the current elapsed value into a list, so you retain both the cumulative time and the delta from the previous lap:

Lap 1:  00:42.180  (+00:42.180)
Lap 2:  01:15.902  (+00:33.722)
Lap 3:  01:58.410  (+00:42.508)

Conclusion

A stopwatch feels trivial until you need it to stay accurate under real-world browser conditions — background tabs, throttled timers, and inconsistent frame rates all conspire to make naive tick-counting drift. Using a monotonic high-resolution clock as the single source of truth, and deriving every displayed value from it, is what separates a reliable chronometer from one that quietly loses milliseconds over a long session.