Understanding JavaScript Memory Leaks and V8 Garbage Collection

This article explains how JavaScript's garbage collector works, outlines typical memory‑leak scenarios, and provides practical techniques—such as reducing global variables, cleaning DOM references, and managing event listeners—to improve performance and avoid UI jank.

ELab Team
ELab Team
ELab Team
Understanding JavaScript Memory Leaks and V8 Garbage Collection

For low‑level languages like C/C++, memory must be allocated and freed manually, while JavaScript developers rely on a garbage collector (GC). Improper code can prevent the GC from reclaiming memory, leading to leaks and UI jank. This article analyzes the GC’s operation, summarizes typical leak scenarios, and offers ways to avoid them.

Memory lifecycle

All programming languages share a similar memory lifecycle: allocation, use (read/write), and release or return.

Why garbage collection is needed

Device memory is limited; without releasing memory, new objects cannot be allocated. This is analogous to not clearing a table after a meal, eventually leaving no space for new diners, causing a program crash.

In most environments, users release memory after use, similar to C/C++ developers freeing allocated memory.

In JavaScript, the GC acts like a service staff that automatically clears unused memory, allowing the limited memory space to be reused.

function grow() {

  var x = []

  let str = new Array(100000).join('x');
  // 1 hundred million
  for (let i = 0; i < 100000000; i++) {
    x.push(str)
  }
}

document.getElementById('grow').addEventListener('click', grow);

The code above creates a massive array of strings; clicking the button quickly exhausts the tab’s memory limit (≈512 MB on 32‑bit Chrome, ≈1.4 GB on 64‑bit).

Variable storage methods

JavaScript variables are divided into primitive types (stored on the stack) and reference types (stored on the heap).

var a = 1;

function doSomething() {
    let b = 2;
    let obj = { c: 3}
    console.log(a, b);
}

doSomething();

In the global execution context, a is a primitive stored on the stack. Inside doSomething, b (primitive) lives on the stack, while obj (reference) lives on the heap.

Stack memory garbage collection

When a function finishes, its execution context is popped from the stack, and all variables in that context are immediately reclaimed.

For reference types, the variable’s reference is removed, but the object may still reside in the heap until the heap GC collects it.

Heap memory garbage collection

Generational hypothesis

The generational hypothesis assumes most new objects die young and are reclaimed within a single GC cycle.

V8 therefore splits the heap into a young generation (Nursery and Intermediate) and an old generation. Objects start in Nursery; surviving objects move to Intermediate after one GC, and after two cycles they are promoted to the old generation.

Major GC

The major (or full) GC performs three basic tasks: marking reachable objects, sweeping (reclaiming memory of unreachable objects), and optionally defragmenting memory.

Marking

During the marking phase, the GC starts from root objects and recursively marks all reachable objects as live.

var obj1 = { a: 1 };
var obj2 = { b: 2 };

After executing obj2 = null;, obj2 loses its reference and will be collected.

obj2 = null;

Sweeping

The GC maintains a free‑list of reclaimed memory blocks and reuses them for future allocations.

Defragmenting

Optionally, the GC compacts live objects into contiguous memory regions to reduce fragmentation.

Minor GC

The minor GC handles the young generation. It works in four steps: marking, copying live objects from the Nursery (from‑space) to Intermediate (to‑space), updating references, and swapping the roles of the two spaces.

Objects that survive two minor collections are promoted to the old generation.

GC execution timing

Initially, GC runs on the main thread, pausing JavaScript execution (Stop‑the‑World). If many objects need processing, the pause can be long, causing noticeable UI lag.

Improving GC efficiency

Goal: free the main thread. Google’s Orinoco GC project proposes three techniques: parallel, incremental, and concurrent collection.

Parallel

Auxiliary threads perform GC work alongside the main thread, reducing pause time.

Incremental

The GC work is broken into small tasks that interleave with JavaScript execution, preventing long pauses.

Concurrent

GC runs entirely on background threads while the main thread continues executing JavaScript, eliminating Stop‑the‑World pauses.

Typical leak scenarios

Understanding V8’s GC helps developers reduce its workload. The key practice is to release variables as soon as they are no longer needed.

Reduce global variables

Undeclared variables in non‑strict mode become properties of the global object, keeping them alive after a function returns.

// In non‑strict mode, bar becomes global
function foo(arg) {
    bar = { a: 1 };
    this.obj = { b: 1 };
    console.log(bar, obj);
}

foo();

Enable strict mode or use linting to catch such errors.

Clean up DOM references

When manually manipulating DOM nodes, clear references after removal to allow GC to reclaim them.

<body>
  <input type="text" id="input">
  <div id="node"></div>

  <script>
    let node = document.getElementById('node');
    node.parentNode.removeChild(node);
    console.log('node', node); // still holds reference
    node = null; // break reference
  </script>
</body>

Event listeners & timers

Always remove listeners or clear timers when a component is unmounted to avoid retaining references.

componentDidMount() {
    this.myScaleBar?.addEventListener('mousedown', this.handleMouseDown);
    document.addEventListener('mousemove', this.handleMouseMove);
    document.addEventListener('mouseup', this.handleMouseUp);
}

componentWillUnmount() {
    this.myScaleBar?.removeEventListener('mousedown', this.handleMouseDown);
    document.removeEventListener('mousemove', this.handleMouseMove);
    document.removeEventListener('mouseup', this.handleMouseUp);
}

How to detect memory leaks

Chrome DevTools’ Performance panel can record memory usage over time. Focus on the JavaScript heap to spot growth that isn’t reclaimed.

Running the earlier grow example shows memory spikes that are reclaimed after a forced GC, indicating no leak. Modifying the code to omit var for x makes x global, so its memory remains after the function returns, demonstrating a leak.

Summary

V8’s garbage collector periodically reclaims unused memory. Excessive object creation increases GC workload and can stall the main thread, causing UI lag. Developers should minimize global variables, promptly release DOM references, and clean up event listeners and timers to reduce GC pressure.

References

Trash talk: the Orinoco garbage collector – https://v8.dev/blog/trash-talk

Generational hypothesis – https://www.memorymanagement.org/glossary/g.html#term-generational-hypothesis

Generational garbage collection – https://www.memorymanagement.org/glossary/g.html#term-generational-garbage-collection

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

FrontendperformanceJavaScriptv8
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.