A browser SPA keeps `const rowMeta = new Map()` at module scope, keyed by the `<tr>` elements it renders, and adds an entry every time a table is drawn. Why does this grow without bound as the user navigates, and does switching it to a `WeakMap` fix it?
answer
- strong keys, no eviction policy
- element keys drag whole tables along
- weak keys let entries disappear
- only objects can be weak keys
- string-keyed caches still need a bound
basics
~20 sA Map holds its keys strongly, so every row element ever rendered stays reachable through the module-level Map and its whole table with it. A WeakMap holds keys weakly and does fix this case, but only for object keys and only if nothing else still references the element.
solid answer
~50 s`Map` keeps a strong reference to every key and value, so the elements can never be collected while the module lives — and because each `<tr>` points at its parent table, the entire detached table is retained too. The map only ever grows. Swapping in a `WeakMap` fixes exactly this shape: its keys are held weakly, so once an element is unreachable everywhere else the entry disappears on its own, with no teardown code. The caveats matter in an interview. `WeakMap` keys must be objects (non-registered symbols became legal in ES2023), so a cache keyed by a URL string cannot use one — that needs an explicit bound, an LRU or a TTL. `WeakMap` is not enumerable and has no `size`, which is the price of weakness. And it fixes nothing if something else still pins the element, such as an array of rendered rows.
code
javascript · 13 linesconst strong = new Map();
const weak = new WeakMap();
let row = document.createElement('tr');
strong.set(row, { height: 32 });
weak.set(row, { height: 32 });
row = null; // the only other reference is gone
// The Map still references the element, so it can never be collected:
console.log(strong.size); // 1
// The WeakMap entry is now eligible for collection; it has no size to report.
console.log('size' in weak); // falsego deeper
Know that a Map keeps everything you put into it until you delete it, and that using DOM elements as keys keeps those elements in memory. Say that a WeakMap holds its keys weakly.
Explain both faults — strong keys and no eviction — and why element keys amplify the cost by retaining detached trees. State the WeakMap constraint that keys must be objects, and that it has no size and no iteration.
Show judgment about which structure fits: weak for derived side data whose lifetime should follow someone else's object, bounded and inspectable for a cache you must invalidate. Point out that weakness is useless while another array pins the same elements.
Own the policy: every long-lived cache in the codebase declares a bound and an invalidation trigger, and that requirement is reviewable. Be ready to argue why unobservable weak structures are a poor default for anything you will later need to debug or size.
## The mechanism A `Map` is a strong container in both directions: it references its keys and its values, and the map itself is referenced by the module scope, which lives as long as the page. The reachability chain is therefore module → map → `<tr>` → parent table → every other row and cell in that table. Nothing in that chain is ever broken by rendering a new table or navigating to another view, so each render adds another permanently retained table. This is the canonical unbounded-cache leak: the code never does anything wrong at the moment it runs, it simply never removes anything. Two separate faults are stacked here, and a strong answer separates them: 1. **Unbounded growth** — the collection has no eviction policy at all. 2. **Amplification** — the keys are DOM elements, so each retained entry drags a whole detached tree with it. A map of a thousand small metadata objects is a nuisance; a map of a thousand table rows is megabytes. ## What WeakMap changes A `WeakMap` holds its keys weakly. If the only reference to an element is its presence as a `WeakMap` key, the collector is free to reclaim it, and the entry vanishes with it. That is precisely the semantics you want for "side data attached to an object I do not own the lifetime of": the cache follows the object rather than outliving it, and you write no teardown code at all. The semantics are ephemeron-based, which handles the obvious trap: if the *value* references its own key, the entry is still collectable. A plain `Map` in that situation would be an immortal cycle from the map's perspective; a `WeakMap` is not. ```js const meta = new WeakMap(); function remember(row) { meta.set(row, { row, renderedAt: Date.now() }); // value points back at the key } // still collectable once `row` is unreachable elsewhere ``` ## What WeakMap does not fix - **Non-object keys.** `WeakMap` keys must be objects — or, since ES2023, non-registered symbols. A response cache keyed by a URL string, a user id or a query hash cannot be weak. That case needs a real policy: a maximum size with LRU eviction, a TTL, or clearing on route change. - **Keys pinned elsewhere.** If the app also holds `const renderedRows = []` containing the same elements, the `WeakMap` is irrelevant — the array keeps them alive and the weak entry with them. Weakness fixes retention *by the cache*, not retention in general. - **Values that reach roots.** The value is held as strongly as the key is reachable. A value holding a subscription or a large buffer still occupies memory for as long as its key lives. - **Observability.** `WeakMap` has no `size`, no iteration, no `clear()`. You cannot inspect it, count it, or write a test that asserts it is empty — which is exactly why it cannot leak, and also why it is a poor choice when you need to know what is cached. ## Choosing between the two Use `WeakMap` (or `WeakSet`) when the entry is **derived data whose lifetime should match an object someone else owns**: metadata for a DOM node, a parsed form of a third-party object, a "have I already processed this" marker. Use a bounded strong structure when the cache is **the point** — a request cache, a memoization table keyed by primitives, anything you need to inspect, size, or invalidate deliberately. "Bounded" means a written-down policy: a cap plus an eviction rule, or a clear on some lifecycle event. ## WeakRef and FinalizationRegistry ES2021 added `WeakRef` (a weak pointer you `deref()`, which may return `undefined`) and `FinalizationRegistry` (a callback after an object is collected). They cover cases a `WeakMap` cannot, such as a weakly held *value* rather than key. Both come with a strong warning in the specification: collection timing is not guaranteed and callbacks may never run, so nothing correctness-critical may depend on them. In practice they are diagnostics and caches, not resource management — closing a socket in a `FinalizationRegistry` callback is a bug, and interviewers listen for whether you know that. ## The answer that lands Name the leak (strong keys plus no eviction), name the amplifier (element keys retain detached trees), say `WeakMap` is the right fix for element-keyed side data, and then volunteer the boundary: string-keyed caches still need an explicit size or time limit, because no amount of weakness helps a key the program keeps re-creating.
- If the value stored in a WeakMap references its own key, does the entry leak?No. WeakMap entries have ephemeron semantics: the value is only treated as reachable while the key is reachable by some other path. A value pointing back at its key therefore does not keep the pair alive, which is the difference from a plain Map, where such a cycle would be retained forever by the map's own strong references.
- What would you use to cache API responses keyed by URL, given that a WeakMap cannot take string keys?A Map with an explicit policy — a maximum entry count with LRU eviction, a TTL, or both — plus a deliberate invalidation point such as route change or logout. The important part is that the bound is written down and testable; an unbounded response cache in a long-lived tab is the same leak in a different shape, just without the detached DOM amplification.
- Why is closing a WebSocket inside a FinalizationRegistry callback a bug?Because the specification guarantees nothing about when — or whether — those callbacks run. Collection may be deferred indefinitely, and callbacks may be skipped entirely when a page is discarded. Treat FinalizationRegistry as a diagnostic that tells you something was collected, and release real resources explicitly in teardown.
saying these in an interview costs you the question
- Thinks a Map drops entries when the key element is removed
- Claims WeakMap works with string or number keys
- Believes WeakMap guarantees prompt or immediate collection
- Says WeakMap fixes a leak even while an array holds the same elements
- Uses FinalizationRegistry to release sockets or file handles