Time & Space Complexity Trade-offs
intermediateBig-O reasoning the way interviewers want it: the Map/Set/object/array cheat sheet, hidden O(n) costs like spread and includes, and trading memory for speed.
Measure JavaScript performance before you optimize: performance.now vs Date.now, the User Timing API, PerformanceObserver, DevTools flame charts, and the micro-benchmark traps that make numbers lie.
You claim your change made the page faster. Prove it. How do you measure, and how do you know the number you got is real?
This is the question underneath every performance interview, and it's the one candidates skip. The senior instinct isn't "I know a faster way" — it's "measure first, optimize the hot path, measure again." Optimizing without a baseline is how people spend a day rewriting code that was never the bottleneck.
The one-liner: you can't optimize what you haven't measured, and you can't trust a measurement you took once.
PerformanceObserver reports field metrics from real devices, not your fast laptop.performance.now(), not Date.now()const start = performance.now();
doWork();
const ms = performance.now() - start;
console.log(`doWork took ${ms.toFixed(2)}ms`);Two reasons performance.now() beats Date.now():
Date.now() follows the system clock, which NTP sync or the user can move mid-measurement, producing negative or wildly wrong durations.Date.now() is integer milliseconds, useless for anything that runs in under ~1 ms.mark and measureAd-hoc performance.now() diffs don't show up anywhere. The User Timing API records named marks and measures onto the same timeline DevTools draws, so your spans appear right next to browser events.
performance.mark("parse-start");
const data = JSON.parse(hugePayload);
performance.mark("parse-end");
performance.measure("parse", "parse-start", "parse-end");
const [entry] = performance.getEntriesByName("parse");
console.log(entry.duration); // ms between the two marksThose "parse" measures render as labeled bars in the Performance panel's Timings track — the cheapest way to answer "which phase is slow?" without a profiler.
PerformanceObserverPolling performance.getEntries() misses events and forces you to guess when to look. PerformanceObserver pushes entries to you as they happen — this is how RUM libraries capture long tasks and paint metrics on real users' devices:
// Anything that blocks the main thread for >50ms is a "long task"
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`Long task: ${entry.duration.toFixed(0)}ms`, entry);
}
});
obs.observe({ type: "longtask", buffered: true });Swap the type for "largest-contentful-paint", "layout-shift", "event" (INP), or "paint" to capture the metrics Google actually ranks on.
performance.now() tells you how long; the flame chart tells you where. Record, then read it top-down:
longtask observer is catching.console.time("label") / console.timeEnd("label") is the two-line version for quick console work; console.profile() / console.profileEnd() starts and stops a CPU profile programmatically.
Most "X is faster than Y" benchmarks are wrong because of how a JIT-compiled, garbage-collected runtime behaves. Guard against all of these:
function bench(fn, iterations = 100_000) {
for (let i = 0; i < 1000; i++) fn(); // warm up the JIT, timing nothing
const runs = [];
for (let r = 0; r < 7; r++) {
const start = performance.now();
let sink; // consume the result so it can't be optimized away
for (let i = 0; i < iterations; i++) sink = fn();
runs.push(performance.now() - start);
if (sink === Symbol()) console.log(sink); // unreachable; keeps `sink` live
}
runs.sort((a, b) => a - b);
return runs[Math.floor(runs.length / 2)]; // median run
}// pseudo
const t1 = time(() => methodA());
const t2 = time(() => methodB()); // runs secondSee this loop applied end to end in Optimize a Slow Function.
performance.now() isn't sub-microsecond precise anymore — why?" Browsers coarsen timer resolution to defend against Spectre-style timing attacks. Run more iterations to recover signal.PerformanceObserver with event (INP, interaction latency) and largest-contentful-paint (LCP) types — the Core Web Vitals that gate search ranking and are best captured in the field.Big-O reasoning the way interviewers want it: the Map/Set/object/array cheat sheet, hidden O(n) costs like spread and includes, and trading memory for speed.
How GC reachability actually works, the four classic leaks, WeakMap/WeakRef, and finding retained memory with heap snapshots.
Why interleaving DOM reads and writes forces synchronous layout, the read/write batching fix, requestAnimationFrame, and compositor-only properties.
A hands-on exercise: profile first, fix the algorithm, hoist invariant work, cut allocations, memoize, then chunk off the main thread — in that order.