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.

7 min read

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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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.

FeatureDate.now()performance.now()
UnitsInteger millisecondsFloat milliseconds
MonotonicNo, clock drift possibleYes
Best useTimestamps, datesBenchmarks, 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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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 typeWhat it measures
longtaskTasks lasting more than 50ms on the main thread
measureCustom named measurements from performance.measure
paintFirst 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:

javascriptjavascript
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:

javascriptjavascript
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.87ms

These 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Why not just use Date.now() for timing?

Date.now() has millisecond resolution and can jump backward or forward due to system clock adjustments. It is fine for coarse timing over seconds or minutes, but unreliable for sub-millisecond measurements.

How precise is performance.now()?

performance.now() returns sub-millisecond values as floating-point numbers. In cross-origin isolated contexts the resolution is 5 microseconds. In normal contexts it is coarsened to 100 microseconds for security.

What is the fastest way to time a single function call?

Use console.time('label') before the call and console.timeEnd('label') after. It prints the elapsed time to the console with no manual math required.

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.