Language Internals
Part 4 of 6 · JS/TS Language ProficiencyMemory, GC, WeakRef/WeakMap & Performance Pitfalls
GC reachability, WeakMap/WeakRef, retention leaks, language-level perf pitfalls.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
When does an object stay alive?
Answer
While any root can reach it through a chain of strong references.
L2
Why is nulling one variable not enough?
Answer
A listener, a Map, a timer, or another closure may still point at the same object.
L3
What does WeakMap hold weakly?
Answer
The key. The value stays strongly reachable for as long as the key is alive.
L4
What can WeakRef.deref return?
Answer
The target, or undefined after the target has been collected. Collection timing is not deterministic.
L5
Why is FinalizationRegistry a bad file closer?
Answer
The callback may run late, on a later turn, or not in the window you needed. Cleanup that must happen belongs in an explicit dispose path.
L6
Does delete free the property's value immediately?
Answer
It removes the own property. The value is collected only if nothing else reaches it and a GC cycle runs. The shape change can also affect optimization.
L7
What do you look for in a heap snapshot?
Answer
Growing retained size, detached DOM nodes, Maps that only ever set, and listener lists. Compare two snapshots after the same action.
Failure modes
Listener closes over a large scope
A long-lived target keeps the handler, and the handler keeps the environment it captured.
Unbounded Map
A cache keyed by id grows for the life of the process because nothing deletes or evicts.
Detached DOM
The node is gone from the document and still referenced from JavaScript, so the subtree stays.
Uncleared timer
An interval closes over state and reschedules forever.
Finalizer used as a destructor
A lock or a file is released only in a finalizer, so the release is late or skipped.
Misconceptions
Assigning null always frees the object.
It drops one edge. Other roots still count.
WeakMap values are weak.
Keys are weak. Values are strong for the lifetime of the key, and a value that points back at the key can keep both.
GC runs at a predictable statement.
The engine decides. Deref and finalizers are observations, not control flow you can sequence.
Interviewer traps
Recommending FinalizationRegistry to close sockets.
Say explicit dispose. Finalizers are a backstop for caches, not RAII.
Putting a strong Map inside a WeakRef cache and still leaking the keys.
A Map from id to WeakRef keeps the id string forever. Bound the key set, or key a WeakMap by the object itself.
Blaming the GC for a long task.
A synchronous loop blocks the realm before any collector runs. Chunk the work or move it off the realm.
Design scenario
Same prompt for every reader.
Requirements
Name the strong roots, say which structure should be a WeakMap, and say what must stay an explicit eviction.
Traffic / scale
Tens of thousands of short-lived objects per minute, plus a smaller set of long-lived DOM or session objects.
Latency
The failure shows up as retained heap and longer GC pauses, not as a wrong return value.
Consistency
Metadata must disappear when the object it describes disappears, unless you still need it by id.
Availability
The process should stay under its memory limit without depending on a finalizer running on time.
Failure assumptions
- Callers forget to delete.
- A closure captures the whole request object.
- GC may not run during the leak you are trying to observe.
Constraints
- Stay on reachability and weak references.
- Do not turn the answer into a profiler product tour.
Prompt
A widget registry Map grows without bound after users navigate away, and a cache of parsed buffers keeps multi-megabyte objects after the caller dropped them.
How should this side table retain keys?
Prefer
WeakMap when the object is the key
Metadata hangs off an object you do not own. When the object becomes unreachable, the entry can go with it.
- Keys are objects, and they are weak.
- You cannot list the keys. That is the point.
- Values stay alive only as long as the key does.
Alternative
Map when you need lookup by id
Strings and numbers are not weak keys. A Map will grow until you evict. TTL or an LRU is part of the design, not an afterthought.
- Both key and value are strong.
- You can iterate, which is why it leaks.
- WeakRef of the value still leaves the key strong.
Decide if it can be collected
One remaining edge from a root is enough to keep the whole graph.
- 1
List the roots
Stack frames, global bindings, and host handles such as timers and registered listeners. - 2
Walk strong edges
Properties, closures, Map entries, and arrays. WeakMap keys and WeakRef targets do not count as strong. - 3
Drop your edge anyway
Null the variable you own. If a listener or a cache still points in, the object stays. - 4
Wait for the engine
Unreachable objects are collected on the engine's schedule, not at the assignment.
Overview
Engines collect garbage from a reachability graph. You rarely call the collector, and you should not need to. The skill is seeing the root you did not mean to keep: a detached node plus its listener, a cache with no bound, a closure over a large scope, a module-level set of subscriptions.
Weak collections exist so you can attach data to an object without becoming the reason it stays alive. They have sharp edges. GC timing is not part of your control flow.
Reachability
Flow
- 1
1. Roots: stack and globals
- next2. Object A
- next4. Listener closure
- 2
2. Object A
- next3. Object B
- 3
3. Object B
- 4
4. Listener closure
- next5. Detached DOM node
- 5
5. Detached DOM node
Lesson map
Memory, GC, WeakRef/WeakMap & Performance Pitfalls
GC reachability, WeakMap/WeakRef, retention leaks, language-level perf pitfalls.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB roots["1. Roots: stack and globals"] a["2. Object A"] b["3. Object B"] c["4. Listener closure"] roots -->|1. Roots: stack and globals| a a -->|2. Object A to 3. Object B| b roots -->|1. Roots: stack and globals| c
If anything reachable from a root references an object, the object survives. "I nulled my variable" is incomplete when another structure still points at it. In the picture, cutting the path to A can free B. C still keeps the detached node because the listener is rooted.
Strong and weak references
| API | What stays strong | Use |
|---|---|---|
Map | Key and value | Caches you evict yourself |
WeakMap | Value, only while the key is alive | Metadata on an object or a DOM node |
WeakSet | Nothing about membership once the object is gone | Tagging objects without retaining them |
WeakRef | Not the target | A cache that must tolerate a miss |
FinalizationRegistry | Not the target | Last-resort notice after collection |
Prefer WeakMap for per-object metadata. Treat WeakRef and FinalizationRegistry as expert tools. Never rely on a finalizer for correctness: locks, file closes, and acknowledgements need an explicit path. A finalizer can run late, and resurrecting an object from one is a subtle way to pin it again.
A value in a WeakMap that points back at the key keeps the key reachable. The weak key does not help if the value is your root.
Retention pitfalls
- Event listeners on a long-lived target that close over a large environment. Removing the listener drops that root.
- Unbounded caches, a
Mapkeyed by id that only eversets. - Detached DOM still referenced from JavaScript.
- Timers that were never cleared, whose callbacks close over state.
- Module singletons that accumulate subscriptions for the life of the process.
- Closures that captured an entire outer object when they needed one field. The same rule applies anywhere a callback outlives the work it was created for.
Language-level cost
These are not a profiler lecture. They are ways the language makes the optimizing compiler and the allocator do more work on the same thread the event loop is trying to share.
| Pattern | Why it hurts | Prefer |
|---|---|---|
| Hidden-class transitions | Adding properties in different orders, or deleting them, changes the shape | Initialize the same properties in the same order |
arguments and with | Older deoptimization traps | Rest parameters, and no with |
| Megamorphic calls | One call site sees many shapes | A helper that sees one shape |
+= on a giant string in a loop | Repeated allocation | Push into an array and join once |
| A long synchronous loop | One turn never ends, so timers and paint wait | Chunk the work, or a Worker (its own loop) |
delete obj.prop removes an own property. It does not schedule a free. It can also change the shape the compiler was using.
WeakMap metadata
Press Run. Snippets must be self-contained — no network, files, or native modules.
The WeakMap entry does not keep node alive by itself. The Map keeps both ids until something deletes them. A production cache needs a bound. This sketch only shows the growth.
WeakRef cache
A Map from a string key to a WeakRef still retains every key. deref() must be allowed to miss. This is illustrative, not a cache you should paste into a service without an eviction policy for the keys.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Interview Q&A
Does delete obj.prop free memory immediately?
Answer
It removes an own property. The value becomes eligible only when nothing else reaches it, and only when the collector runs. Deleting a property can also change the object's shape and undo an optimization.
Why is FinalizationRegistry dangerous for resource management?
Answer
Collection can be delayed for a long time. The callback runs on a later turn, not at the lexical end of a block. Resurrecting the object from the callback pins it again. Close files and release locks with an explicit dispose path.
How do you find leaks in practice?
Answer
Take heap snapshots, compare retained size, and look for detached DOM, Maps that only grow, and listener lists. Reproduce with an allocation timeline. The model you bring to the snapshot is reachability. The tool confirms which root you missed.
What does WeakMap not solve?
Answer
Lookup by string id. Keys must be objects. You cannot iterate the map to build a report. If the value points at the key, the entry stays. A cache of buffers by request id is still a Map with an eviction policy.
Map or WeakMap for DOM node metadata?
Answer
WeakMap, keyed by the node. When the node is unreachable aside from that map, the entry can be collected. A Map keyed by node would keep every node you ever tagged.
Why can a closure be a leak?
Answer
The closure is reachable from a root (a listener, a timer, a module). It keeps its environment. If the environment holds a large object the callback does not need, that object stays. Narrow what you capture.
Is a long task a GC problem?
Answer
Not yet. A synchronous loop occupies the call stack, so the event loop cannot take the next task and the collector is not your first suspect. Break up the work. Then look at allocation rate if pauses remain.
What do you say in the first minute?
Answer
Alive means reachable from a root. Nulling one variable drops one edge. Map retains keys. WeakMap does not retain object keys. WeakRef can be empty. Finalizers are not destructors. Unbounded caches, listeners, timers, and wide closures are the usual roots.
Pitfalls
- Treating
deleteornullas a free. - Caching in a
Mapwith no eviction and calling it a leak in the GC. - Using
WeakRefand forgetting the map of keys is still strong. - Depending on
FinalizationRegistryto unlock a mutex. - Storing a value that points back at a WeakMap key.
- Explaining a multi-second synchronous loop as "GC pressure" before you look at the stack.
Sketch three objects: a DOM node, a listener function, and a 10 MB buffer the listener closed over. Mark which references are strong. Remove the node from the document and say whether the buffer can be collected. Then swap the listener's side table from Map to WeakMap and say what changed.