Skip to Content
Course content

120: Debugging Memory Issues in DevTools

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We’ve all been there. You’ve built a feature that feels snappy during development, but after an hour of poking around in the app, you notice the fans on your laptop starting to scream. The UI gets jittery, and if you open the Task Manager, your browser tab is eating 2GB of RAM for a page that should be using 100MB. This is the classic memory leak, and it's one of the most frustrating bugs to squash because the code doesn't actually "crash"—it just slowly suffocates.

The "Guess-and-Check" approach

When I first started dealing with memory issues, my instinct was to play a game of elimination. I'd look at the parts of the code that handled large amounts of data—like a data table or a chat window—and start commenting out blocks of logic to see if the memory usage stopped climbing. It's a naive approach because you're essentially guessing. If you have a leak in a utility function that's called from twenty different places, commenting out one feature won't tell you anything.

Another common mistake is thinking that setting a variable to null is a magic bullet. While it can help, it doesn't solve the problem if that object is still being referenced elsewhere. I remember spending a whole afternoon nulling out a large array in a UserSession manager, only to realize the array was still trapped in a closure inside a setInterval callback. The memory stayed allocated because the timer was still ticking, and the timer had a reference to the scope containing that array.

// The kind of mistake that hides from "guess-and-check"
function startSessionTracker() {
  const heavyData = new Array(1000000).fill('🚀'); 
  
  setInterval(() => {
    // I'm not even using heavyData here, but because this 
    // closure exists, heavyData can never be garbage collected.
    console.log("Session still active...");
  }, 1000);
}


Hunting the Retainers in the Heap

The professional way to handle this is to stop guessing and start looking at the Heap. In Chrome DevTools, the Memory tab is your best friend, but it's intimidating because it throws a lot of terminology at you. The trick is to use Heap Snapshots to find what's actually sticking around.

Instead of looking at the total memory, I want you to focus on the "Comparison" view. You take a snapshot, perform the action that you suspect is leaking (like opening and closing a modal ten times), and then take a second snapshot. When you switch the view from "Summary" to "Comparison," DevTools shows you exactly what objects were created between those two points and weren't cleaned up. This narrows your search from "everything in the app" to "the specific objects that are leaking."

But seeing that (array) or (system / Context) is leaking isn't enough. You have to look at the Retainers panel at the bottom. This is the "smoking gun." The retainers list shows you the chain of references that is preventing the Garbage Collector (GC) from reclaiming that memory. If you see a path that leads back to a global variable or a forgotten event listener, you've found your leak.

The trade-off: Precision vs. Overhead

Now, you might wonder why we don't just keep the Memory profiler running all the time. The trade-off is performance. Taking a heap snapshot literally pauses the JavaScript execution engine to walk through every single object in memory. It's a "stop-the-world" event. If you do this in a production environment, your users will see the app freeze for several seconds.

This is why we use snapshots for deep dives, but use the Allocation Instrumentation on Timeline for spotting trends. The timeline view shows you blue bars as memory is allocated. If you see a staircase pattern—where memory goes up, drops slightly, then goes up higher—you know you have a leak. You don't know what is leaking yet, but you know when it's happening. I usually start with the timeline to find the trigger, and then use the snapshot to find the culprit.




📋 Practical Task

Fixing the Leaky Dashboard Event Registry

You have been handed a legacy dashboard module that suffers from a severe memory leak. The module uses a Registry object to track active widgets. Every time a widget is created, it adds a listener to the window object, but the cleanup logic is flawed.

The Goal: Modify the provided code so that when destroyWidget() is called, the widget is fully eligible for garbage collection. You must ensure that both the internal Registry map and the window event listeners are properly cleared.


const Registry = {
  activeWidgets: new Map(),
};

function createWidget(id) {
  const widget = {
    id,
    data: new Array(100000).fill('Data Point'),
    onResize: () => {
      console.log(`Widget ${id} resizing...`);
    }
  };

  // Store widget in registry
  Registry.activeWidgets.set(id, widget);

  // Add global listener
  window.addEventListener('resize', widget.onResize);
  
  return widget;
}

function destroyWidget(id) {
  const widget = Registry.activeWidgets.get(id);
  if (widget) {
    // TODO: Fix the leak here. 
    // Currently, the widget is removed from the map, 
    // but the window listener still holds a reference to widget.onResize.
    Registry.activeWidgets.delete(id);
  }
}

// Simulation: Creating and destroying widgets repeatedly
for(let i = 0; i < 100; i++) {
  const w = createWidget(i);
  destroyWidget(i);
}


Verification: If you run this in Chrome, take a Heap Snapshot before the loop and after the loop. If the (array) objects from the widgets are still present in the snapshot after destroyWidget has been called, the leak persists.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.