"How would you find a memory leak" and "this list is slow, what do you do" are senior questions because they test method: knowing what the garbage collector can and cannot free, knowing the handful of patterns that leak, and measuring with the right tool before changing code. The memory-leaks section covers Nuxt and SSR specifics; this page is the language-level model.
window, document, a store) never removed; closures retaining large scopes; detached DOM nodes still referenced from JavaScript; unbounded caches (Map or array used as a cache with no eviction); accidental globals; subscriptions (observers, sockets, stores) not disposed; console.log of objects in DevTools keeping them alive; growing arrays in module scope on a server.addEventListener(..., { signal }) with one AbortController per owner; onScopeDispose/onUnmounted for everything registered in a component; WeakMap for object-keyed caches; bounded LRU caches with size or TTL; clearInterval in cleanup; disconnect() on observers; avoid module-level mutable state on servers.performance.measureUserAgentSpecificMemory() for RUM, process.memoryUsage() and --heap-snapshot-near-heap-limit in Node.find/includes inside a loop is O(n²) and a Map index makes it O(n); Set.has beats Array.includes; string concatenation in a loop is fine (ropes), but JSON.parse/stringify and structuredClone of large objects are not free.delete on hot objects, initialise fields in the constructor), monomorphic call sites, avoid arguments and try/catch in the hottest inner loops only when profiling says so. Micro-optimisations come last.scheduler.yield()/setTimeout, move CPU work to workers, debounce and throttle input handlers, virtualise long lists, and memoise derived data (a computed in Vue) instead of recomputing in render.performance.mark/measure and performance.now() for code, the Performance panel's flame chart for main-thread time, Lighthouse and RUM (web-vitals) for user impact. Benchmark with repeated runs and a warm-up; a single console.time is noise.The classic leaks and their fixes, side by side:
export function useTicker(onTick: () => void) {
// ✗ never cleared: the interval (and the closure, and the component) live forever
// setInterval(onTick, 1000)
// ✓ owned by the component scope
const { pause } = useIntervalFn(onTick, 1000)
onScopeDispose(pause)
}
export function bindShortcuts(handlers: Record<string, () => void>) {
const controller = new AbortController()
// One signal for every listener: aborting removes them all, including ones added later.
window.addEventListener('keydown', e => handlers[e.key]?.(), { signal: controller.signal })
window.addEventListener('blur', () => console.debug('lost focus'), { signal: controller.signal })
return () => controller.abort()
}
// A cache with a bound. Map preserves insertion order, so the first key is the least recently inserted.
export class LRU<K, V> {
#map = new Map<K, V>()
constructor(private readonly max = 100) {}
get(key: K): V | undefined {
const value = this.#map.get(key)
if (value === undefined) return undefined
this.#map.delete(key) // move to the end: most recently used
this.#map.set(key, value)
return value
}
set(key: K, value: V) {
this.#map.delete(key)
this.#map.set(key, value)
if (this.#map.size > this.max) this.#map.delete(this.#map.keys().next().value!) // evict the oldest
}
}
// Object-keyed memo that cannot leak: entries die with their keys.
const layoutCache = new WeakMap<HTMLElement, DOMRect>()
export function measure(el: HTMLElement) {
return layoutCache.get(el) ?? layoutCache.set(el, el.getBoundingClientRect()).get(el)!
}
Turning an O(n²) render helper into O(n), and proving it:
function joinSlow(orders: Order[], users: User[]) {
return orders.map(o => ({ ...o, user: users.find(u => u.id === o.userId) })) // n × m
}
function joinFast(orders: Order[], users: User[]) {
const byId = new Map(users.map(u => [u.id, u] as const))
return orders.map(o => ({ ...o, user: byId.get(o.userId) })) // n + m
}
for (const fn of [joinSlow, joinFast]) {
fn(orders, users) // warm-up
performance.mark('start')
for (let i = 0; i < 10; i++) fn(orders, users)
performance.mark('end')
console.log(fn.name, performance.measure(fn.name, 'start', 'end').duration / 10, 'ms')
}
WeakMap keys must be objects and the map is not iterable, so it cannot implement LRU or size limits, and it cannot cache by string key. It is right for "attach data to this object for as long as the object lives"; for everything else use a bounded Map.
The widest bar in a profile is often "(program)" or GC, caused by allocation churn elsewhere. Read the profile bottom-up, find your code that allocates or recomputes, fix that, re-measure. Guessing at micro-optimisations without a profile is how teams spend a sprint saving two milliseconds.
setInterval fifty times; take heap snapshots before and after and find the retainer path. Fix it and confirm the delta is flat.LRU class and a test that inserts max + 1 entries and asserts which key was evicted after a get reorders."Reproduce it as a repeatable action: open and close a view twenty times. Take a heap snapshot before and after, use the comparison view to see which constructors grew, and follow the retainers of one of those objects back to a root; the path names the culprit, typically a listener on window, an interval, a closure holding a large scope, a detached DOM node or a cache without bounds. The fix follows the pattern: remove listeners with an AbortSignal or in onScopeDispose, clear timers, cap caches or key them weakly, and stop keeping per-request data at module level on the server. Then I repeat the snapshot comparison to prove the growth is gone, and if it matters I add a test that measures heap growth over repetitions."
"Measure before touching code: a Performance profile of the interaction to see whether the time is in JavaScript, layout or paint, and how long the tasks are. If it is JavaScript, I look for algorithmic problems first, nested lookups that a Map index turns linear, derived data recomputed in render that a computed would cache, and deep reactivity on large arrays that shallowRef avoids. If it is rendering, I check for layout thrashing, too many DOM nodes, and virtualise the list. Long tasks get chunked or moved to a worker so input stays responsive. Then I re-measure the same interaction, because the goal is a number moving, not a feeling."
Nothing yet. Notes filed here show up on this page and the section overview.
Collections, iterators and generators
Map, Set and their 2025 methods, WeakMap and WeakRef, the iteration protocol, generators and async iteration, iterator helpers, grouping and upsert, change-by-copy arrays, typed arrays and base64, and web streams.
Modern JavaScript, ES2024 to ES2026
What each of the last three editions added, what is finished for ES2027, the stage 3 proposals worth knowing by name, and how Baseline, build targets and polyfills decide what you can ship unpolyfilled.