The clocks a browser exposes
Three distinct time sources are visible to any page, and each answers a different question:
- The wall clock -
Date.now()andnew Date()report civil time from the operating system. It can jump backwards or forwards when the system clock is adjusted, and it carries the timezone offset. - The monotonic clock -
performance.now()counts milliseconds since the document's time origin and never goes backwards. It is deliberately independent of system clock changes, which is what makes it useful as a control. - The navigation origin -
performance.timeOriginis an absolute wall-clock timestamp for the moment the monotonic clock started, which is what ties the other two together.
The identity that must hold is straightforward: timeOrigin + now() should reconstruct Date.now() to within a few milliseconds. On an unmodified browser the residual is small and stable. Adjusting the reported wall clock without adjusting the origin, or vice versa, breaks the identity immediately, and the size of the break is often exactly the adjustment that was applied.
Rate, grid, and cross-API agreement
Beyond the single reconciliation, three further checks add independent confirmation.
Rate agreement. Sample both clocks, wait, and sample again. The elapsed wall time and the elapsed monotonic time should match closely. A one-off offset survives this check; a clock that runs at a different rate does not. Because the two counters are read through different code paths, keeping both rates aligned requires changing both consistently.
Grid coherence. Browsers deliberately coarsen high-resolution timers to limit timing attacks, so performance.now() values land on a quantisation grid whose resolution depends on the browser, its version, and whether cross-origin isolation is active. Every PerformanceEntry timestamp - resource loads, marks, measures - is produced by the same clock and must land on that same grid. A value that sits off the grid was produced by something other than the clock it claims to come from.
Cross-API agreement. Navigation Timing exists in two generations that both describe the same page load: the legacy performance.timing object with absolute epoch timestamps, and the modern PerformanceNavigationTiming entry with values relative to timeOrigin. Converting between them must produce the same milestones in the same spec-defined order. This is a straight cross-API coherence check applied to time.
Why clocks get adjusted, and what it costs
Clocks are adjusted for two ordinary reasons. The first is locale consistency: an exit IP in one region and a system timezone from another produces a timezone and IP mismatch, and the obvious fix is to change the reported timezone. The second is throughput: scripts that shorten timers to make a page finish faster inevitably touch time.
Both are safe when done at the right layer and expensive when done at the wrong one. Changing the timezone at the operating-system or engine level moves Date, Intl, and timeOrigin together, so every check above continues to reconcile. Overriding Date in page JavaScript moves one clock and leaves performance untouched, which breaks the reconstruction identity, and also leaves a non-native Date constructor for native function integrity checks to find.
The general rule matches the rest of this cluster: time is a property of the environment, so it should be set where the environment is defined. Aligning the exit IP, the system timezone, and the accepted languages at the infrastructure layer removes the reason to touch the clock from JavaScript at all - which is how a managed web data API keeps locale coherent without creating a timing anomaly in the process.