As a frontend or Node.js developer, we frequently need to measure time: animation frame rates, network request latency, code execution performance... Most people immediately think of Date.now(). However, it only offers millisecond precision and is susceptible to system clock adjustments. As web applications demand higher performance, microsecond (μs) measurement has become essential for profiling, game development, and fine-grained animations. This article takes you deep into JavaScript's high-precision time API — performance.now(). From underlying principles to security limitations, and from code examples to microsecond concepts, we will thoroughly demystify the world of time in the browser.
1. Limitations of the Traditional Timer: Date.now()
Date.now() returns the number of milliseconds since January 1, 1970, 00:00:00 UTC. While simple to use, it has two fatal flaws:
- Low Precision: It returns integers. It cannot measure operations shorter than 1ms (e.g., an empty loop might take only 0.05ms).
- Non-Monotonic: If a user changes the system time or if NTP synchronization adjusts the clock, the value of
Date.now()can jump backwards or forwards, leading to measurement errors (e.g., the end timestamp being smaller than the start timestamp).
let start = Date.now(); // e.g., 1678901234567
// Some operation...
let end = Date.now(); // Could be less than start if the clock goes back2. Enter the High-Precision API: performance.now()
performance.now(), part of the Performance interface, returns a high-resolution floating-point number. While the unit is still milliseconds, the decimal portion can represent microseconds or even nanoseconds. Its core advantages are:
- Monotonically Increasing: Unaffected by system clock adjustments; it always increases (unless the number range is exceeded, which is practically impossible).
- High Resolution: Typically accurate to within 5 microseconds (depending on hardware and browser implementation).
- Page-Load Anchored: Relative to
performance.timeOrigin, making it ideal for measuring events within the same origin.
Basic Usage:
let t0 = performance.now();
// ... Code block to measure ...
let t1 = performance.now();
console.log(`Execution time: ${t1 - t0} ms`); // Might output 0.123456789 ms3. From Milliseconds to Microseconds: Understanding Time Units
To grasp the "high precision" of performance.now(), let's review the time scales:
| Unit | Symbol | Conversion to Seconds | Common Scenarios |
|---|---|---|---|
| Second | s | 1 s = 1,000 ms | Daily timing |
| Millisecond | ms | 1 ms = 10⁻³ s | Date.now() precision, network latency |
| Microsecond | μs | 1 μs = 10⁻⁶ s = 0.001 ms | Audio sampling intervals, hardware response |
| Nanosecond | ns | 1 ns = 10⁻⁹ s = 0.001 μs | CPU instruction cycles |
The integer part of performance.now() represents milliseconds, while the decimal part represents microseconds (e.g., 12.345678 ms = 12 ms and 345.678 μs). Although the specification doesn't mandate a specific precision, modern browsers typically provide at least 5 microseconds (0.005ms) resolution.
4. Practical Comparison: Date.now() vs. performance.now()
Let's demonstrate the precision difference using a very short empty loop:
// Measure an empty loop 1000 times
let d1 = Date.now();
for (let i = 0; i < 1000; i++) {}
let d2 = Date.now();
console.log(`Time via Date.now: ${d2 - d1} ms`); // Likely outputs 0 or 1 ms
let p1 = performance.now();
for (let i = 0; i < 1000; i++) {}
let p2 = performance.now();
console.log(`Time via performance.now: ${p2 - p1} ms`); // e.g., 0.0899999999 msIn Chrome, Date.now() almost always returns 0 (indicating <1ms), whereas performance.now() provides a value like 0.089 ms, truly capturing the tiny execution time.
5. Underlying Principles of performance.now()
Internally, browsers typically base performance.now() on one of the following low-level timers:
- Windows:
QueryPerformanceCounter()(QPC) for high-precision timing. - Linux:
clock_gettime(CLOCK_MONOTONIC)for monotonic time. - macOS/iOS:
mach_absolute_time()converted to nanoseconds. - Web Abstraction: Browsers wrap these into a stable floating-point millisecond value.
These underlying APIs provide microsecond or even nanosecond resolution and guarantee monotonicity.
6. Typical Use Cases: From Profiling to Game Loops
Function Execution Time
Measure the exact duration of algorithms or DOM operations to identify bottlenecks. Ideal for automated performance regression testing.
Delta Time Calculation
Use high-precision time within requestAnimationFrame to calculate the interval between frames, ensuring smooth physics simulations regardless of frame rate fluctuations.
Benchmark.js, etc.
All serious JavaScript benchmarking libraries rely on performance.now() for microsecond-level sampling.
Precise Scheduling
The Web Audio API relies internally on high-precision time to ensure audio buffers are filled on schedule.
6.1 Delta Time in Animations
let lastTime = performance.now();
function tick() {
let now = performance.now();
let delta = now - lastTime; // Interval between frames (ms), used for distance calculation
lastTime = now;
// Update animation position
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);7. Security Restrictions: Why is Precision Sometimes Reduced?
Following the exposure of the Spectre vulnerability in 2018, browser vendors actively reduced the resolution of high-precision timers to prevent side-channel attacks. By default, the precision of performance.now() may be limited to 100 microseconds or even 1 millisecond, depending on the browser and platform.
Current Mainstream Restrictions:
- Chrome: Default precision is 100 microseconds (0.1ms), with added random jitter to further mitigate timing attacks.
- Firefox: Default precision is 1ms, sometimes 0.5ms.
- Safari: Default precision is 1ms.
If your application genuinely requires higher raw precision (e.g., internal performance tools), you can restore it via Cross-Origin Isolation. Set the following two HTTP headers:
Cross-Origin-Opener-Policy: same-origin
Once enabled, the browser lifts precision restrictions on APIs like performance.now(), returning hardware-level microsecond time.
8. Compatibility and Fallbacks
In very old browsers (e.g., IE9), performance.now() may not exist. In such cases, you can use a fallback based on Date.now(), though you will lose high precision and monotonicity:
var perfNow = (function() {
if (window.performance && window.performance.now) {
return function() { return performance.now(); };
} else {
var start = Date.now();
return function() { return Date.now() - start; }; // Loses global monotonicity but retains relativity
}
})();9. Advanced: User Timing API & performance.mark
Beyond simple now() calls, the Performance API provides mark() and measure() for creating readable performance timelines:
performance.mark('start');
// Execute some task
performance.mark('end');
performance.measure('Task Duration', 'start', 'end');
let entries = performance.getEntriesByName('Task Duration');
console.log(entries[0].duration); // Microsecond-precise durationThis data can be visualized in the Performance panel of Chrome DevTools.
10. The Precision Ceiling: Smallest Representable Value?
performance.now() returns a double-precision floating-point number (64-bit). IEEE 754 can precisely represent integers up to 2^53 (approx. 900 billion days). For relative time (differences usually within a few seconds), the decimal precision remains at the microsecond level (10⁻⁶) or better, fully satisfying daily measurement needs.
11. Real Measurement Examples: Microsecond Differences
The following code shows the tiny duration of different operations:
let t = performance.now();
let a = 1 + 2;
let t2 = performance.now();
console.log(`Addition operation time: ${(t2 - t) * 1000} μs`); // Might be 0.5 μsNote: Single extremely short operations may be noisy; it's common to run loops and take an average.
12. Considerations and Pitfalls
- Floating-Point Errors: Subtracting two large numbers can lead to "loss of significance." However, since
performance.now()starts at page load (values usually within hundreds of seconds), severe precision loss is rare. - Measurement Overhead: Calling
performance.now()itself has a tiny overhead (tens to hundreds of nanoseconds), which should be considered when measuring extremely short operations. - Browser Inconsistencies: Precision and monotonic guarantees vary slightly, but all modern browsers follow the specification.
- Node.js Environment: Node.js also supports
performance.now()(viaperf_hooks), and default precision is often higher since browser security restrictions don't apply.
Conclusion
From the rough milliseconds of Date.now() to the microsecond granularity of performance.now(), JavaScript developers can finally capture every pulse of their code with precision. Whether building high-performance animations, conducting granular performance profiling, or developing time-sensitive audio applications, performance.now() is an indispensable tool. Understanding its underlying principles, precision limits, and security strategies empowers you to develop complex web applications with greater confidence.
Remember: When you need to measure extremely short durations or require monotonic increasing time, embrace performance.now(). It opens the door to the microsecond world.