Skip to main content

Measuring JS code performance

JS performance is measured with three different tools that answer three different questions: a timer in the code tells you how long an operation takes, a profiler tells you where the time actually goes, and a microbenchmark tells you which implementation is faster. A separate layer is real user metrics: long tasks, FPS and Core Web Vitals, because the goal is not a "fast algorithm" but a responsive interface.

Theory

TL;DR

  • Quick timing: performance.now(), performance.mark() / performance.measure(), console.time(). In Node.js the same comes from the node:perf_hooks module.
  • Profiling: the Performance tab in DevTools gives a flame chart, FPS and long tasks; Memory gives heap snapshots; Coverage shows how much JS actually runs.
  • Node.js is profiled with node --inspect plus Chrome DevTools, or node --prof plus prof-process, or clinic.js and 0x.
  • Microbenchmarks are written with Benchmark.js or tinybench: they do the JIT warm-up, many iterations and the statistics for you.
  • UI responsiveness: the Long Tasks API catches tasks longer than 50 ms, requestAnimationFrame gives FPS.
  • Page metrics: Navigation and Resource Timing (TTFB, resource loading), Core Web Vitals (LCP, CLS, INP, TBT), Lighthouse for a one-off check.
  • Method matters more than the tool: warm-up, fixed data, many runs, median and quantiles rather than the mean.

Quick example

javascript
// 1. The simplest precise timer, in milliseconds with a fractional part const t0 = performance.now(); buildIndex(items); const t1 = performance.now(); console.log(`Time: ${(t1 - t0).toFixed(2)} ms`); // 2. The same through the User Timing API: marks stay in the DevTools timeline performance.mark('A'); buildIndex(items); performance.mark('B'); performance.measure('build-index', 'A', 'B'); console.table(performance.getEntriesByName('build-index')); // 3. The shortest option for debugging console.time('op'); buildIndex(items); console.timeEnd('op');

Quick timings right in the code

This is instrumentation: you place a timer around the region you care about yourself.

Browser

javascript
// Precise timer (in ms, with fractions) const t0 = performance.now(); // ... code ... const t1 = performance.now(); console.log(`Time: ${(t1 - t0).toFixed(2)} ms`);
javascript
// User Timing API: marks and measures performance.mark('A'); // ... code ... performance.mark('B'); performance.measure('my-op', 'A', 'B'); console.table(performance.getEntriesByName('my-op'));
javascript
// Fast and simple console.time('op'); // ... code ... console.timeEnd('op');

performance.now() returns high resolution monotonic time counted from the start of the page, so it does not jump when the system clock changes, unlike Date.now(). The advantage of performance.mark() over a bare timer is that the marks land in the profiler timeline, so you see your own measurement next to the browser's real events.

Node.js

javascript
// perf_hooks, monotonic timers const { performance, PerformanceObserver } = require('node:perf_hooks'); performance.mark('A'); // ... code ... performance.mark('B'); performance.measure('op', 'A', 'B'); new PerformanceObserver(list => console.table(list.getEntries())) .observe({ entryTypes: ['measure'] });

Profiling: finding where it is slow

A timer says "slow", but it does not say "because of what". That is what a profiler is for.

Browser DevTools

  • Performance (or Performance Insights): record, and you get a flame chart (hot functions), FPS, layout and recalculate style, long tasks (>= 50 ms).
  • Memory: heap snapshot and allocation instrumentation, used to hunt leaks and excess garbage.
  • Coverage: how much JS and CSS actually executes, which feeds straight into TBT.

Node.js

  • node --inspect plus Chrome DevTools, which gives a CPU profile and heap data.
  • node --prof plus prof-process, or clinic.js or 0x for convenient flame graphs.

You should read the flame chart itself, not only the summary numbers: a wide bar at the bottom is the function the program spent most time in, and that is the one to optimise first.

Microbenchmarks: comparing two implementations

When the question is "which of these two functions is faster", a bare performance.now() lies: the first run goes through unoptimised code until the JIT warms up.

  • Benchmark.js and tinybench do the warm-up, many iterations and the statistics for you.
javascript
import Benchmark from 'benchmark'; const suite = new Benchmark.Suite(); function a(arr){ /* variant A */ } function b(arr){ /* variant B */ } suite .add('A', () => a(data)) .add('B', () => b(data)) .on('cycle', e => console.log(String(e.target))) .on('complete', function () { console.log('Faster:', this.filter('fastest').map('name')); }) .run({ async: true });

Important: warm the test up (discard the first run), fix the input data, do several runs, close extra tabs and extensions.

Interface and page responsiveness metrics

A user does not feel the milliseconds of one function, a user feels freezes and jank.

  • Long Tasks API: catching main thread blocks.
javascript
new PerformanceObserver((list) => { for (const e of list.getEntries()) { console.log('Long task', e.duration.toFixed(1), 'ms'); } }).observe({ entryTypes: ['longtask'] });
  • rAF and FPS: a simple smoothness indicator.
javascript
let last = performance.now(), frames = 0; function tick(ts){ frames++; if (ts - last >= 1000) { console.log('FPS:', frames); frames = 0; last = ts; } requestAnimationFrame(tick); } requestAnimationFrame(tick);

Page level, network and render metrics:

  • Navigation and Resource Timing (performance.getEntriesByType('navigation' | 'resource')), the source of TTFB and resource load times.
  • Core Web Vitals (LCP, CLS, INP, TBT), collect them with the web-vitals library on real traffic.
  • Lighthouse (one-off), good at catching regressions, but it does not replace field metrics from real users.

Method: how to measure correctly

  1. State the goal: are you after CPU, GC, layout or the network? The tool follows from that.
  2. Isolate the context: if you measure a pure algorithm, take the network and the DOM out of the picture.
  3. Warm up the JIT: do a few throwaway runs before the measured one.
  4. Repetitions and statistics: median and quantiles beat the mean, because a single random stall ruins an average.
  5. Control the noise: fix the data and the input size, limit background processes.
  6. Look at the flame (flame chart), not only at numbers, and optimise the hot bottlenecks.
  7. Check for regressions in CI: a small benchmark plus threshold values.

What to measure in typical scenarios:

ScenarioWhat to measure with
Algorithms or collectionsMicrobenchmark (Benchmark.js) plus performance.now()
UI interactivityDevTools Performance, Long Tasks, FPS
Render and stylesThe Rendering, Layout and Styles panes in the profiler; avoid layout thrashing
Memory and leaksHeap snapshots, allocation timeline
Node APIperf_hooks, CPU profile, auditing sync methods (replace with async or worker_threads)

Common mistakes

  • A single eyeballed run: noise and a cold JIT distort the picture.
  • Microbenchmarking DOM operations: the result depends on the real tree and styles, so profile those in DevTools instead of a benchmark.
  • Comparing two implementations on different input data or different input sizes.
  • Optimising without a flame chart: it is easy to fix the wrong place and gain nothing.
  • Taking the mean instead of the median: one garbage collector outlier makes both variants look "the same".
  • Using Date.now() for short measurements: the resolution is coarse and the clock is not monotonic.
  • Trusting lab Lighthouse only and never collecting field Core Web Vitals from real devices.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.