A Stopwatch That Doesn't Drift: performance.now vs Date.now
August 28, 2026 · DevTools
The naive way to build a stopwatch is to add a fixed increment every time a setInterval callback fires — tick every 40ms, add 40ms to the total. It looks right and drifts steadily wrong, because browsers don't guarantee setInterval fires exactly on schedule; background tab throttling, garbage collection pauses, and OS scheduling jitter all eat into the interval, and every missed millisecond compounds. The Precision Stopwatch avoids that entirely by never accumulating time in the first place.
Elapsed time as subtraction, not addition
Each tick recomputes elapsed time as performance.now() - startedAt, where startedAt is captured once when you press start. performance.now() is a monotonic clock — unlike Date.now(), it can't jump backward from an NTP sync or a manual clock change, and it isn't affected by how often the tick actually fires. Whether the 40ms interval fires exactly on time or is a few milliseconds late doesn't matter: the displayed time is always the true difference between now and start, so there's nothing to drift.
Why pausing needs the same trick
Pausing and resuming reset startedAt to performance.now() - elapsed, effectively rewinding the reference point by however much time had already elapsed — so the next tick's subtraction picks up exactly where it left off, rather than needing separate "accumulated" and "current session" counters that could fall out of sync.
Lap splits inherit the same accuracy
Each lap just records the current elapsed value at the moment you press "Lap" — since that value is never approximated, the gap between two laps is exact down to the millisecond the tick interval samples at. For a fixed-duration countdown instead of counting up, see the Fullscreen Countdown Timer, which uses the same absolute-timestamp approach in reverse; for structured work/break cycles, the Pomodoro Focus Timer builds on the same timing discipline.