Measure first, always
Every engineer has spent a day optimising something that turned out to be 2 % of the runtime. The instinct about what is slow is wrong often enough that acting on it without a profile is gambling.
And the arithmetic is unforgiving: if a function is 20 % of the total, making it infinitely fast gains you 20 %. That is Amdahl’s law, and it is the first thing to check before starting — what is the best case, and is it worth the day?
Self time and total time
A profiler gives two numbers per function. Total (or "inclusive") is the time spent in it and everything it called. Self (or "exclusive") is the time spent in its own body.
A function at the top of the total list is often just a caller — main is always 100 %. The function to optimise is the one high on the self list; that is where the CPU actually was.
total self function
1000 100 root
400 60 parse
250 250 tokenize ← the actual work
90 90 build
500 80 render
300 20 layout
280 280 measure ← and here
120 120 paint
The panel, briefly
- Record only the interaction you care about. A 60-second trace is unreadable; three seconds around one click is not.
- Throttle the CPU to 4× or 6× and the network to Slow 4G. Otherwise you are profiling your own laptop, which nobody uses.
- Flame chart = the call stack over time; width is duration, depth is nesting. Bottom-up = the self-time list. Start with bottom-up, then find it in the flame chart to see who calls it.
- Purple is layout and paint, yellow is scripting. A wall of purple after your JavaScript means layout thrashing, not slow code.
- Look for repetition. The same stack fifty times is usually one loop doing something per item that could be done once.
Marking your own code
performance.now() is a monotonic clock in fractional milliseconds — it never jumps backwards the way Date.now() can. In cross-origin-isolated pages the resolution is 5 µs; otherwise it is deliberately coarsened to about 100 µs to make timing attacks harder, which is another reason a single measurement of something fast is meaningless.
For production, the Long Animation Frames API (observe({ type: "long-animation-frame" })) is the one to know: it reports slow frames with the script that caused them, which the old Long Tasks API never did.
performance.mark("render:start");
renderTable(rows);
performance.mark("render:end");
const { duration } = performance.measure("render", "render:start", "render:end");
/* the same names appear in the Performance panel's timings track,
and you can ship them: */
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) report(entry.name, entry.duration);
}).observe({ type: "measure", buffered: true });
Micro-benchmarks lie
- Warm up. The first runs are interpreted; the JIT optimises later ones. Measuring the first is measuring the wrong thing.
- Use the result. An engine will delete a computation whose result nothing reads.
- Take the median of several runs, never one — and compare medians, never best cases.
- Beware the toy input. An algorithm that wins on ten items can lose on ten thousand, and vice versa.
- Then measure the real page anyway. A function that is twice as fast and 0.3 % of the load is a rounding error.