Ask a browser for the time and it will hand back a number with three decimal places, which looks authoritative until you try to rely on it. Millisecond precision in a web page is real but conditional: it depends on which clock you read, whether the tab is visible, and whether you are asking what time it is or how long something took. Those are different questions with different answers, and mixing them up is the source of most timing bugs on the web.
This article separates the two clocks a browser exposes, explains why one of them can jump and the other cannot, and shows why animation frames make a better heartbeat than intervals. The precision seconds counter is built on exactly these rules, so it is a useful thing to watch while reading.
What Does Millisecond Precision Mean In A Browser?
Millisecond precision means the time value is reported in thousandths of a second. It says nothing about how correct that value is, and nothing about how often the page can actually act on it, which are the two things that usually matter more.
Three separate limits are at work. Resolution is how finely the number is expressed. Accuracy is how close it sits to true time. Latency is the gap between the browser learning the time and the pixel changing on screen. A page can have perfect resolution and still be a frame behind, and it can be a frame behind and still be entirely fit for showing a clock, because a frame at 60 Hz is under 17 milliseconds.
Date.now() Against performance.now()
Date.now() returns wall-clock time as milliseconds since 1 January 1970 UTC, while performance.now() returns a fractional number of milliseconds since the page's own time origin. The first tells you the time of day; the second tells you how much time has elapsed.
The differences are worth listing plainly:
- Date.now(): follows the system clock, so it reflects any correction, manual change or time zone adjustment the operating system applies. It can move backwards.
- performance.now(): is one of the browser's monotonic clocks. It only ever increases, is unaffected by system clock changes, and carries sub-millisecond resolution.
- Epoch versus origin: Date.now() counts from a fixed universal epoch shared by every machine, while performance.now() counts from when this particular document started. Two tabs will not agree on it.
- Deliberate coarsening: browsers reduce timer resolution as a defence against timing side-channel attacks, so performance.now() is rounded rather than reporting the hardware's finest granularity.
The practical rule is short. Both offer millisecond precision, but only one of them is safe for durations: use Date.now() to display the time, and performance.now() to measure a span. A stopwatch that measures with wall-clock time will report a negative lap if a synchronisation correction lands mid-run.
Why Monotonic Time Matters
A monotonic clock is one that can never go backwards, no matter what happens to the calendar. That single guarantee is what makes elapsed-time measurement trustworthy, because the alternative is a value that can be edited underneath you.
System clocks are edited more often than people expect. Network Time Protocol corrections can step the clock; a laptop waking from suspend can resynchronise abruptly; a user can set the date by hand; a virtual machine can inherit a host correction. Any of those events breaks a duration computed by subtracting two wall-clock readings. Monotonic clocks are immune by construction, which is why every serious stopwatch, profiler and animation loop in the browser is built on one. The trade-off is that a monotonic reading is meaningless as a time of day, so a page that wants millisecond precision on both questions simply keeps both clocks.
Why Do Timers Drift In Background Tabs?
Because browsers deliberately throttle them to save power. When a tab is hidden, setInterval and setTimeout are typically clamped to roughly one callback per second, and after a tab has been in the background for several minutes the clamp can tighten much further, to something closer to once a minute.
requestAnimationFrame goes further still and simply stops. There is nothing to paint on a hidden tab, so the browser stops delivering frames, and a clock built on animation frames freezes until the tab is visible again. This is correct behaviour, not a bug, but it has consequences: a countdown that counts ticks will finish late, sometimes by minutes, if the tab was buried. A countdown that stores a target instant and re-derives the remaining time on each wake-up is unaffected, because it never depended on the ticks arriving on schedule.
Designing Around Throttling
The defence is to derive rather than accumulate. Record the start or the deadline once, then compute the difference from a fresh reading every time the page repaints. Any missed frames simply vanish from the arithmetic instead of accumulating into an error. For anything that must be watched continuously, keep the tab in the foreground and use a fullscreen display mode, which also holds the screen awake.
Why Animation Frames Beat Intervals
requestAnimationFrame is scheduled against the display refresh rate, so the browser calls back immediately before it paints. On a 60 Hz screen that is roughly every 16.7 milliseconds, and on a 120 Hz screen roughly every 8.3, which means updates land in step with what the panel can actually show.
setInterval offers no such alignment. Its delay is a minimum, not a promise, so setInterval drift accumulates whenever the main thread is busy with layout, network work or garbage collection. A one-second interval that runs a few milliseconds late every time will visibly slip within the hour, and because it lands out of step with the refresh cycle, the seconds change at an inconsistent point in each frame. Animation frames also cost nothing when nobody is looking, since they stop with the tab, and they deliver millisecond precision at exactly the moment the pixels change. The wider consequences of the system clock underneath are covered in browser clock accuracy.
Getting Reliable Millisecond Precision In Practice
A handful of habits produce a display that holds up over hours rather than minutes.
- Read fresh every frame: ask for the current time on each repaint instead of adding a fixed increment to the last value.
- Store instants, not counts: keep a start time or a deadline and derive everything else from it.
- Match the display, not the clock: there is no value in updating faster than the display refresh rate, since the extra work is never seen.
- Show only the digits people can read: millisecond precision is useful for timing, but a churning third decimal is unreadable on a wall display and merely adds motion.
Those rules are what separate a clock that still agrees with the wall after a long session from one that has quietly slipped. The idea of a continuously recomputed display is covered further in what live time is, and the network side of accuracy in NTP vs browser time. For hand timing, a stopwatch with laps applies the monotonic rule so laps stay consistent.
Conclusion
Millisecond precision in the browser is straightforward once the two clocks are kept apart. Date.now() answers what time it is and follows the system clock, jumps included. performance.now() answers how long something took, never moves backwards, and belongs to the family of monotonic clocks. Timers throttle in hidden tabs and animation frames stop altogether, so derive values from stored instants rather than counting ticks, and paint in step with the refresh rate. Do that and millisecond precision holds up over hours. Watch it work on the precision seconds counter, or see the rest of the displays on livetime.now.