I needed a simple stopwatch in the browser for timing user interaction flows during usability testing. I thought it would take 10 minutes to build. It took two days to get right, and the reason is that JavaScript timers are fundamentally unreliable for precision timing.
The problem with setInterval
The naive approach is obvious:
let elapsed = 0;
const interval = setInterval(() => {
elapsed += 10;
display(elapsed);
}, 10);
This looks like it increments every 10 milliseconds. It does not. setInterval guarantees a minimum delay of 10ms, not an exact delay. The actual interval depends on the browser's event loop, current tab focus state, CPU load, and garbage collection pauses.
On a busy page, a "10ms" interval might fire every 14-20ms. Over 60 seconds, that drift accumulates to several seconds of error. Your stopwatch shows 60 seconds, but 63 seconds have actually passed.
The correct approach: wall clock timestamps
Instead of counting intervals, you record the start time and compute elapsed time on every tick:
const startTime = performance.now();
function update() {
const elapsed = performance.now() - startTime;
display(elapsed);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
performance.now() gives you a high-resolution timestamp that is not affected by system clock adjustments. Date.now() works too, but it is only millisecond-precision and can jump if the system clock is adjusted.
Using requestAnimationFrame instead of setInterval ties your display updates to the screen refresh rate (typically 60Hz), which is sufficient for a visual display and more efficient than hammering the DOM every 10ms.
The background tab problem
When a browser tab is in the background, most browsers throttle timers aggressively. setInterval might fire only once per second. requestAnimationFrame stops entirely.
For a stopwatch, this is a critical issue. You start the timer, switch to another tab, come back, and the display is behind.
The fix is straightforward with the wall-clock approach: when the tab regains focus, you simply recalculate elapsed time from the original start timestamp. The display catches up instantly.
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
update(); // Recalculate from startTime
}
});
Lap times and split tracking
A stopwatch without lap functionality is barely useful for real timing work. The implementation is simple: on each lap, record the current elapsed time. Display both the total elapsed time and the delta from the previous lap.
const laps = [];
function recordLap() {
const current = performance.now() - startTime;
const previous = laps.length > 0 ? laps[laps.length - 1] : 0;
laps.push(current);
displayLap(current, current - previous);
}
The UX details matter more than the code. Fastest and slowest laps should be highlighted. The lap list should auto-scroll but allow the user to scroll back without fighting the auto-scroll. Export to CSV is essential for any serious timing work.
Pause and resume without drift
Pausing introduces another subtle bug opportunity. If you just store the elapsed time at pause and restart the clock, you need to offset the start time:
let pauseOffset = 0;
let pauseStart = null;
function pause() {
pauseStart = performance.now();
}
function resume() {
pauseOffset += performance.now() - pauseStart;
pauseStart = null;
}
function getElapsed() {
const now = pauseStart || performance.now();
return now - startTime - pauseOffset;
}
This ensures that paused time is not counted toward elapsed time, regardless of how long the pause lasts.
Why I built a web-based stopwatch
Phone stopwatches work fine for casual timing. But for development work -- timing API calls, user flows, build processes -- I want the timer on the same screen I am working on, with keyboard shortcuts, lap tracking, and the ability to copy results.
I built one at zovo.one/free-tools/stopwatch that handles all the edge cases: accurate wall-clock timing, background tab resilience, lap tracking with highlights, keyboard controls, and clean data export. It is the stopwatch I wished existed the first time I tried to time a user session.
I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.
Top comments (0)