How to Measure JavaScript Execution Time Accurately
Date.now() is not enough for precise timing. Learn when to use console.time, performance.now, and the Performance Observer API for accurate execution measurement.
To measure JavaScript execution time accurately, you need more than a stopwatch mentality: finding what makes an application slow, and confirming that a fix actually helped, both require the right tool. JavaScript gives you three timing tools, each with different precision, convenience, and gotchas.
The main question is how precise you need to be. For a quick sanity check on a slow function, console.time is enough. For benchmarking algorithms at the microsecond level, you need performance.now.
console.time and console.timeEnd
Call console.time with a label to start a timer, and console.timeEnd with the same label to stop it. The elapsed time prints directly to the console with no manual math:
console.time("sortArray");
const sorted = largeArray.sort((a, b) => a - b);
console.timeEnd("sortArray"); // "sortArray: 3.421ms"You can run multiple labeled timers at once, which is useful when you want to compare two phases of the same operation side by side:
console.time("fetch");
const response = await fetch("/api/data");
console.timeEnd("fetch"); // "fetch: 187.234ms"
console.time("parse");
const data = await response.json();
console.timeEnd("parse"); // "parse: 4.102ms"This drop-in pair of calls is perfect for quick investigation, since the precision matches the same high-resolution timer that performance.now uses under the hood. One limitation is that the output only goes to the console, so you cannot capture it programmatically in an automated test or in production.
performance.now for high-resolution timing
performance.now returns a floating-point number of milliseconds since the page started loading. Unlike Date.now, it has sub-millisecond precision and uses a monotonic clock that never goes backward:
const start = performance.now();
doExpensiveWork();
const end = performance.now();
console.log(`doExpensiveWork took ${end - start}ms`);The returned value is a double-precision float accurate to microseconds. For most practical work, the difference between two calls is what matters, not the raw value itself.
Date.now returns whole milliseconds since the Unix epoch, and it can jump forward or backward if the system clock is adjusted through an NTP sync or a manual change. performance.now is monotonic, meaning it only increases and is unaffected by clock changes, which is why it is the safer choice for measuring a duration.
| Feature | Date.now() | performance.now() |
|---|---|---|
| Units | Integer milliseconds | Float milliseconds |
| Monotonic | No, clock drift possible | Yes |
| Best use | Timestamps, dates | Benchmarks, animation |
Security limits on precision
Browsers coarsen performance.now to prevent timing side-channel attacks like Spectre. On a normal page the resolution is limited to 100 microseconds. On a cross-origin isolated page, set up with the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers, the resolution improves to 5 microseconds.
For most applications, 100 microseconds is precise enough. You can check which mode the current page is running in:
if (crossOriginIsolated) {
console.log("High-resolution timing available");
} else {
console.log("Standard resolution");
}The crossOriginIsolated global is a boolean the browser sets automatically based on the response headers, so you never need to compute it yourself.
Measuring a function reliably
A single measurement of a fast function is noisy, because JavaScript engines have warm-up time from JIT compilation and occasional garbage collection pauses. For benchmarking, run the function many times and take an average instead of trusting one call:
function measure(fn, iterations = 10000) {
for (let i = 0; i < 100; i++) fn(); // warm up so the JIT can optimize
const start = performance.now();
for (let i = 0; i < iterations; i++) fn();
const end = performance.now();
return (end - start) / iterations;
}The warm-up loop matters because the first few runs of a function may be slow while the engine interprets it. After enough iterations, the JIT compiler optimizes the function and later timings stabilize:
function sumReduce(numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
const perCall = measure(() => sumReduce([1, 2, 3, 4, 5]), 100000);
console.log(`${perCall.toFixed(6)}ms per call`);Running this against a tiny five-item array typically reports a per-call time in the range of a few thousandths of a millisecond, since the warm-up loop has already let the engine optimize the hot path before the real measurement starts.
Performance Observer for continuous monitoring
For ongoing monitoring without polling, use the PerformanceObserver API. It asynchronously notifies you about events like long tasks and paint timing, so there is no setInterval and no measuring overhead of its own:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn(`Long task: ${entry.duration.toFixed(1)}ms`);
}
}
});
observer.observe({ type: "longtask", buffered: true });This fires whenever a task takes longer than 50ms, the threshold the Long Tasks API uses for work that can cause visible jank. The table below lists the entry types you are most likely to use:
| Entry type | What it measures |
|---|---|
| longtask | Tasks lasting more than 50ms on the main thread |
| measure | Custom named measurements from performance.measure |
| paint | First paint and first contentful paint timing |
performance.mark and performance.measure
For timing across multiple points in a longer flow, place named markers with performance.mark and compute the difference between two of them with performance.measure:
performance.mark("start-render");
renderHeader();
performance.mark("header-done");
renderContent();
performance.mark("content-done");Once the markers exist, measure creates a named duration between any two of them, and getEntriesByType reads every measurement back out at once:
performance.measure("header", "start-render", "header-done");
performance.measure("content", "header-done", "content-done");
const measures = performance.getEntriesByType("measure");
measures.forEach((m) => console.log(`${m.name}: ${m.duration.toFixed(2)}ms`));
// header: 2.31ms
// content: 12.87msThese named measurements also appear in the Chrome DevTools Performance panel timeline, which makes them useful for correlating console output with visual profiling later.
When to use each tool
Start with console.time for most day-to-day debugging. Move to performance.now once you need programmatic access to the result or sub-millisecond precision. For ongoing production monitoring, use PerformanceObserver so the check runs passively instead of on a timer.
For more on using the Chrome developer tools to find bottlenecks, see the DevTools performance tuning guide. For the types of errors that can show up in timing code, see the JavaScript error types guide.
Rune AI
Key Insights
- Use console.time() and console.timeEnd() for quick, readable timing output.
- Use performance.now() for sub-millisecond precision in benchmarks and profiling code.
- Avoid Date.now() for sub-second measurements; it has low resolution and can jump due to clock drift.
- performance.now() resolution depends on cross-origin isolation: 5 microseconds isolated, 100 microseconds otherwise.
- Performance Observer watches long tasks and paint timing passively without polling.
Frequently Asked Questions
Why not just use Date.now() for timing?
How precise is performance.now()?
What is the fastest way to time a single function call?
Conclusion
Measuring execution time accurately means choosing the right tool for the precision you need. console.time is the fastest for quick checks. performance.now gives you microsecond-level floating-point timestamps for benchmarks. Performance Observer lets you monitor long tasks passively. Date.now is unreliable for sub-second measurements, and performance.now has security-driven precision limits you should understand.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.