Katkat

Diagnose a Memory Leak in a JavaScript Web Application

AndurilFrontend Engineer · Onsite

Last updated

Tests your ability to identify common JavaScript memory leak patterns—such as detached DOM trees, uncleared event subscriptions, and unbounded closures—and verify resolution using browser heap profiling techniques.

Scenario

You are investigating a performance degradation report in a long-running operational dashboard. Operators leave the application open for 12+ hours during drone and sensor monitoring missions. Over time, the browser tab consumes over 2 GB of memory, becomes sluggish, and eventually crashes with an Out of Memory error.

You have isolated the issue to a dynamic telemetry widget that mounts and unmounts whenever the user switches between sensor feeds. Below is a simplified version of the component implementation:

class TelemetryStreamer {
  constructor(sensorId, containerElement) {
    this.sensorId = sensorId;
    this.container = containerElement;
    this.cache = [];
    
    // Create UI element
    this.displayEl = document.createElement('div');
    this.displayEl.className = 'telemetry-readout';
    this.container.appendChild(this.displayEl);

    // Global event listener for incoming websocket telemetry packets
    window.addEventListener('sensor-update', (e) => {
      if (e.detail.id === this.sensorId) {
        this.render(e.detail.data);
      }
    });

    // Periodic stats polling
    this.timer = setInterval(() => {
      this.cache.push({ timestamp: Date.now(), status: 'active' });
    }, 1000);
  }

  render(data) {
    this.displayEl.textContent = `Sensor ${this.sensorId}: ${JSON.stringify(data)}`;
  }

  destroy() {
    // Invoked when switching sensor feeds
    this.container.removeChild(this.displayEl);
  }
}

Tasks

  1. Identify the Leaks: Analyze the TelemetryStreamer class and identify all distinct mechanisms preventing instances and associated DOM elements from being garbage collected after destroy() is called.
  2. Provide a Leak-Free Implementation: Refactor TelemetryStreamer to ensure all references, timers, and listeners are properly decoupled and cleaned up upon teardown.
  3. Diagnostic Strategy: Describe the exact steps you would take in Chrome DevTools (or equivalent browser tooling) to:
    • Confirm the leak using heap snapshots.
    • Identify detached DOM trees and retaining paths.
    • Validate that memory usage reaches a stable steady state after your fix.

Hints

Hint 1

Check what roots keep the instance alive: look at global objects (`window`), long-running timers, and anonymous closures.

Hint 2

Removing a child element from the DOM via `removeChild` does not free it if an active JavaScript reference or closure still retains that element.

Hint 3

To isolate leaks in DevTools, compare two heap snapshots taken before and after repeated mount/unmount cycles, filtering by 'Objects allocated between snapshots'.

Follow-up questions

  • How does using `AbortController` simplify event listener cleanup compared to manually storing and removing bound handler references?
  • When would you choose `WeakMap` or `WeakRef` over an explicit `.destroy()` lifecycle method for caching DOM elements or telemetry state?
  • How can you automate memory leak detection in CI/CD pipelines before code reaches production?