Users feel jank and slow loads; guessing at causes wastes time while measurement finds them.
The first rule of performance is measure. Intuition about what's slow is usually wrong, and engines optimise ordinary code very well.
Tools:
performance.now()gives a high-resolution timestamp for quick timings;performance.mark()andperformance.measure()create named entries that show up in DevTools traces.- The DevTools Performance panel records CPU, layout, paint and long tasks; the Memory panel takes heap snapshots.
- In Node.js,
node --cpu-profwrites a profile, and--inspectconnects Chrome DevTools. - Benchmark with realistic data sizes and repeat runs; the first iterations include JIT warm-up.
The usual culprits, in order of payoff:
- Algorithmic complexity:
array.includesorfindinside a loop is O(n²); build aSetorMapfirst. - Doing too much on the main thread: a task over 50 ms is a "long task" that delays input. Chunk work, yield to the event loop, or move it to a Web Worker.
- Layout thrashing: alternating DOM writes (
el.style.width = ...) with reads that need layout (offsetHeight,getBoundingClientRect) forces repeated synchronous layout. Batch all reads, then all writes, or userequestAnimationFrame. - Too much work per event: debounce or throttle
input,scrollandresizehandlers; use{ passive: true }for scroll/touch listeners. - Network and bundle size: lazy-load with dynamic
import(), compress, cache.
Micro-optimisations — caching array.length, choosing for over forEach — rarely matter compared with these. Write clear code, measure, then optimise the hot spot the profile shows.
Example
// Run with: node perf.mjs
const N = 20_000;
const ids = Array.from({ length: N }, (_, i) => i);
const wanted = Array.from({ length: N }, (_, i) => i * 2);
function time(label, fn) {
performance.mark(`${label}-start`);
const result = fn();
const m = performance.measure(label, `${label}-start`);
console.log(`${label}: ${m.duration.toFixed(1)} ms (${result} hits)`);
}
time("array.includes (O(n^2))", () =>
wanted.filter(id => ids.includes(id)).length);
time("Set.has (O(n))", () => {
const idSet = new Set(ids);
return wanted.filter(id => idSet.has(id)).length;
});
// The Set version is typically dozens of times faster at this size.Remember
- Profile first; optimise what the measurements show
- Fix complexity (Set/Map lookups) and long main-thread tasks before micro-tuning
- Batch DOM reads and writes to avoid forced synchronous layout
Watch out for
- Benchmarking a function once, cold, and drawing conclusions from JIT warm-up noise.