Skip to Content
Course content

137: Memory Leaks and How to Avoid Them

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

Most of us treat JavaScript's garbage collector like a magic butler—we just throw things away and assume they disappear. For the most part, that works. But the garbage collector only cleans up memory that is "unreachable." The moment you accidentally keep a reference to a piece of data that you no longer need, you've created a memory leak. In a small script, you won't notice. In a long-running Single Page Application (SPA), these leaks accumulate until the browser tab slows to a crawl or crashes entirely.

The 'Set It and Forget It' Trap

I see this most often with global event listeners. Imagine you're building a custom notification toast that needs to reposition itself whenever the browser window is resized. The naive way to handle this is to just attach a listener to the window the moment the toast is created.

function createNotification(message) {
  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.innerText = message;
  document.body.appendChild(toast);

  const reposition = () => {
    console.log('Repositioning toast...');
    toast.style.top = `${window.scrollY}px`;
  };

  // The leak starts here
  window.addEventListener('resize', reposition);

  // We provide a way to remove the toast from the UI
  toast.onclick = () => {
    toast.remove();
  };
}

On the surface, this looks fine. When the user clicks the toast, it's removed from the DOM. You might think, "Great, the toast is gone, the memory is reclaimed." But here's the problem: the window object still holds a reference to the reposition function, and that function holds a reference to the toast element via its closure. Even though the toast isn't in the DOM anymore, it's still sitting in memory because the window is still listening for a resize event to trigger a function that needs that toast.

Why the Garbage Collector Can't Help You Here

The garbage collector is essentially looking for a path from the "root" (the global object) to your data. In the example above, the path is: window → event listeners list → reposition function → toast element. As long as that path exists, that div and all its associated data are "reachable," so the GC is forbidden from touching them. If your app creates and destroys dozens of these notifications over a session, you're effectively leaking DOM nodes and memory every single time.

Taking Control of the Lifecycle

To fix this, you have to be explicit. You cannot trust the environment to know when a piece of logic is no longer relevant. The professional approach is to ensure every "setup" has a corresponding "teardown."

function createNotification(message) {
  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.innerText = message;
  document.body.appendChild(toast);

  const reposition = () => {
    toast.style.top = `${window.scrollY}px`;
  };

  window.addEventListener('resize', reposition);

  toast.onclick = () => {
    // Clean up the listener BEFORE removing the element
    window.removeEventListener('resize', reposition);
    toast.remove();
  };
}

Now, when the toast is clicked, we sever the link between the global window and our local function. The path is broken, the reposition function becomes unreachable, and the toast element can finally be swept away. If you're working in a modern framework, this is exactly why you have lifecycle hooks like useEffect cleanup functions or onDestroy methods.

There is a slight trade-off here: you're writing more code and you have to be disciplined. It's easy to forget to remove one listener in a complex component. However, the alternative is a "zombie" application that gets slower the longer a user keeps it open. I'd much rather spend an extra ten seconds writing a removeEventListener call than spend three days debugging a vague "out of memory" report from a user in production.




📋 Practical Task

Fixing the Zombie Stock Ticker

You are maintaining a dashboard that has a "Live Stock Ticker" widget. The widget uses setInterval to fetch new prices every second. However, the developers noticed that even after the ticker is closed, the network requests keep happening in the background, and memory usage continues to climb.

Your Task: Modify the closeTicker function in the code below to properly stop the timer and prevent the memory leak.

const stockTicker = {
  timerId: null,
  uiElement: null,

  start: function() {
    this.uiElement = document.createElement('div');
    this.uiElement.innerText = 'Fetching prices...';
    document.body.appendChild(this.uiElement);

    // This interval creates a closure that keeps the ticker object alive
    this.timerId = setInterval(() => {
      console.log('Fetching latest stock prices...');
      this.uiElement.innerText = `Price: $${(Math.random() * 100).toFixed(2)}`;
    }, 1000);
  },

  closeTicker: function() {
    // TODO: Fix the memory leak here
    this.uiElement.remove();
    this.uiElement = null;
  }
};

// Simulate usage
stockTicker.start();
setTimeout(() => {
  stockTicker.closeTicker();
  console.log('Ticker closed. If you still see "Fetching latest stock prices..." in the console, the leak is still there!');
}, 3500);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.