JavaScript / Advanced: scheduling and trustworthy interfaces / Lesson 15
Memory ownership and evidence-based performance
Reason about retained objects, bounded caches, algorithmic work, and browser measurements. Improve a lookup path without inventing timing results or relying on garbage-collection schedules.
On this page
Lesson goals
- Trace reachability and release owned references.
- Bound caches and avoid accidental retention.
- Compare algorithms and measurements with explicit scope.
Automatic collection does not remove reachable objects
Garbage collection reclaims storage according to engine rules when objects are no longer needed through reachable references; applications cannot rely on an exact collection time. A global array, listener closure, or cache can retain an otherwise abandoned view. Deleting one local reference does not help if another owner still points to the same object. Use a lifecycle model: who creates the resource, who registers callbacks, and who releases them? WeakMap can associate metadata without strongly retaining its object keys, but it is not an iterable cache with a predictable eviction schedule and cannot replace explicit lifecycle management.
A cache needs bounds and invalidation
Caching trades repeated work for retained memory and consistency obligations. Define a maximum size, a key contract, an invalidation rule, and whether values are shared mutable objects. The example implements a tiny insertion-order eviction policy: adding a third distinct key removes the oldest inserted key. It is not an LRU cache because reading a key does not refresh its position. Naming the policy prevents a teaching shortcut from becoming an accidental production guarantee. Include rejected loads and obsolete values in a real cache design so failures or stale authorization data do not remain indefinitely.
const cache = new Map();
function remember(key, value) {
cache.set(key, value);
if (cache.size > 2) cache.delete(cache.keys().next().value);
}
remember("A", 1); remember("B", 2); remember("C", 3);
console.log([...cache.keys()].join(","), cache.size);
B,C 2
A is evicted on the third insertion. No timing or garbage-collection behavior is asserted.
Count work before claiming speed
A repeated linear search scans records for every requested identifier. Building an index once can reduce repeated lookup work, at the cost of extra memory and update consistency. The small deterministic example counts comparisons in the linear version and checks identical business results from a Map. It does not measure wall-clock performance or prove a specific engine complexity bound. Tiny datasets may not benefit in elapsed time because index construction itself costs work. Consider representative list sizes, lookup counts, update frequency, and memory constraints before adopting a more elaborate structure.
const rows = [{ id: 1 }, { id: 2 }, { id: 3 }];
const wanted = [3, 1, 3];
let comparisons = 0;
const linear = wanted.map(id => rows.find(row => {
comparisons++; return row.id === id;
}));
const index = new Map(rows.map(row => [row.id, row]));
const indexed = wanted.map(id => index.get(id));
console.log(comparisons);
console.log(linear.every((row, i) => row === indexed[i]));
7
true
Seven comparisons arise from scans of three, one, and three. Equality checks preserve identity of the same matching records.
Measure an end-to-end hypothesis
Use the browser Performance APIs and profiler for a defined hypothesis: a filter action spends too much time building nodes, or retained views grow after repeated opening. Record device, data size, build, warm-up conditions, and repeated measurements. Separate JavaScript execution, layout, painting, network waiting, and user-perceived completion. A faster function can still worsen total interaction if it creates more DOM work. Preserve correctness checks when measuring; skipping validation or omitting rows can make a broken implementation look fast. Report variability and scope instead of quoting a single synthetic run as a universal benchmark.
Responsiveness and memory share ownership decisions
Batch related DOM writes and avoid repeatedly alternating layout-dependent reads with writes when a simpler calculation is possible. Bound queued asynchronous work and discard data no longer needed by the current view. Pagination or virtualization can reduce live DOM size, but must preserve focus, semantics, and findability for users. Test repeated mount and unmount cycles with synthetic data and inspect whether retained objects return to a stable pattern. Memory snapshots need interpretation; a larger heap at one instant is not by itself proof of a leak. Establish a reproducible retaining path or growth pattern before choosing a fix.
Common errors and fixes
A cache grows with every unique request
- Why it happens
- No size or lifetime policy exists.
- How to fix it
- Add a documented bound and invalidation strategy.
- Prevent it next time
- Test repeated distinct keys and stale-value behavior.
Deleting one variable is expected to force collection
- Why it happens
- Reachability through other owners is ignored.
- How to fix it
- Find retaining paths and release owned registrations or references.
- Prevent it next time
- Review setup and teardown as one lifecycle.
A tiny timing result becomes a universal claim
- Why it happens
- Dataset, host, variability, and correctness are omitted.
- How to fix it
- Measure representative end-to-end work and report conditions.
- Prevent it next time
- Require hypothesis and baseline before optimization.
Check your understanding
1. Why is the cache example not LRU?
Show worked solution
Eviction follows insertion order and reads do not update it. An LRU policy must track recency deliberately, for example by moving a read key in an owned ordering structure. Add tests for read refresh and overwrite behavior before claiming an LRU contract.
2. A heap snapshot is larger after one modal closes. Is that enough to prove a leak?
Show worked solution
No. Collection timing and other allocations vary. Reproduce repeated open/close cycles, inspect retaining paths and owner cleanup, and determine whether relevant retained objects grow without a valid owner. Use the evidence to fix the lifecycle rather than forcing collection as an application policy.
Key takeaways
- Bound retained state and identify its owner.
- Measure correct user outcomes before making performance claims.