🧠 Understanding Memory Leaks in JavaScript – With Real-World Examples & Solutions

KUNTAL MAITYKUNTAL MAITY
8 min read

If you're a JavaScript developer, you might have heard the term "memory leak" tossed around in performance discussions. But what exactly is a memory leak? How does it happen in JavaScript, and more importantly, how can you avoid it?

In this blog post, we'll break down the concept of memory leaks in simple, beginner-friendly language, walk through common scenarios, and explore real-world solutions to fix and prevent them.


📦 What is a Memory Leak?

A memory leak happens when your application uses memory but never releases it — even when it's no longer needed. Over time, this causes your application to consume more and more memory, eventually slowing down or crashing.

JavaScript is a garbage-collected language, which means the JS engine automatically frees memory that's no longer reachable. However, this only works if you don't leave accidental references to data you no longer use.


🔎 How JavaScript Memory Works

JavaScript memory is managed in two main parts:

  • Stack Memory – for primitive values (like numbers and strings).
  • Heap Memory – for objects, arrays, functions, and anything non-primitive.

The garbage collector periodically scans the heap and cleans up any data that can’t be reached anymore. A memory leak happens when some object remains reachable even though your program no longer needs it.


⚠️ Common Causes of Memory Leaks in JavaScript

Let’s walk through real examples that cause memory leaks — and how to fix them.

1. ❌ Accidental Global Variables

function startLeak() {
  leakedVar = "I'm a global variable"; // forgot to use let/const
}

startLeak();

What's happening:

  • leakedVar is declared without let, const, or var, so it's automatically attached to the global window object.
  • It stays in memory until the page reloads.

Solution: Always use let, const, or var to declare variables.

function startLeak() {
  const leakedVar = "I'm local now!";
}

2. ❌ Closures Holding on to Large Variables

function outer() {
  let largeArray = new Array(1000000).fill("data");

  return function inner() {
    console.log("Using closure");
  };
}

const hold = outer();

What's happening:

  • inner is returned and stored in hold, keeping a reference to outer's scope.
  • The huge largeArray is never collected, even if it’s unused.

Solution: Avoid unnecessary closures or manually dereference large unused variables.

function outer() {
  let largeArray = new Array(1000000).fill("data");

  function inner() {
    console.log("Using closure");
  }

  largeArray = null; // allow garbage collection
  return inner;
}

3. ❌ Detached DOM Elements

let element = document.getElementById("myDiv");
document.body.removeChild(element);

// Still holding reference
let ref = element;

What's happening:

  • You've removed the element from the DOM, but a reference still exists (ref), so the memory isn’t freed.

Solution: Nullify references once DOM elements are removed.

document.body.removeChild(element);
element = null;

4. ❌ Unstopped Timers or Intervals

function startTimer() {
  let username = "Kuntal";
  setInterval(() => {
    console.log(`Hi ${username}`);
  }, 1000);
}

startTimer();

What's happening:

  • setInterval holds a reference to username forever unless you call clearInterval().

Solution: Always clean up intervals and timeouts when no longer needed.

const timerId = setInterval(() => {
  console.log("Running...");
}, 1000);

// Later
clearInterval(timerId);

In frameworks like React, clean intervals in useEffect:

useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);
  return () => clearInterval(id); // cleanup
}, []);

5. ❌ Forgotten Event Listeners

const button = document.getElementById("clickMe");

function handleClick() {
  console.log("Clicked");
}

button.addEventListener("click", handleClick);

// Later: DOM node removed
button.remove(); // But event listener still holds reference

What's happening:

  • The event listener holds a reference to button, even though it's no longer in the DOM.

Solution: Remove event listeners before removing DOM nodes.

button.removeEventListener("click", handleClick);
button.remove();

6. ❌ Caching Data Without Limits

const cache = {};

function cacheData(key, value) {
  cache[key] = value; // No limit on cache size
}
function getCachedData(key) {
  return cache[key];
}

// Usage
cacheData("largeData", new Array(1000000).fill("data"));
getCachedData("largeData");

What's happening:

  • The cache object grows indefinitely, consuming more memory as you cache more data.

Solution: Implement a cache eviction strategy, like LRU (Least Recently Used) or use WeakMap for temporary data.

Note: A WeakMap in JavaScript is a collection of key-value pairs where the keys must be objects and the values can be of any data type. Its primary distinction from a regular Map is that its keys are "weakly referenced." This means that if an object used as a key in a WeakMap is no longer referenced anywhere else in the application, it becomes eligible for garbage collection, and its corresponding entry in the WeakMap will also be removed.

const cache = new WeakMap();

function cacheData(key, value) {
  cache.set(key, value);
}
function getCachedData(key) {
  return cache.get(key);
}

// Usage
const key = {}; // must be an object
cacheData(key, new Array(1000000).fill("data"));
getCachedData(key);

Note: WeakMap keys must be objects, not primitives like strings.


🛠️ Real-World Example: Memory Leak in Action

✅ HTML + JavaScript Demo (Browser Memory Leak)

You can run this in your browser or tools like JSFiddle, CodePen, or just paste it in your browser console inside a test HTML page.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Memory Leak Demo</title>
</head>
<body>
  <div id="container">
    <button id="add">Add Leaky Element</button>
    <button id="cleanup">Clean References</button>
  </div>

  <script>
    // This array will simulate a memory leak
    const leakyStorage = [];

    document.getElementById("add").addEventListener("click", () => {
      const div = document.createElement("div");
      div.textContent = "Leaky div " + new Date().toLocaleTimeString();
      document.body.appendChild(div);

      // Remove from DOM, but still hold reference in array
      document.body.removeChild(div);
      leakyStorage.push(div); // Memory leak!
    });

    document.getElementById("cleanup").addEventListener("click", () => {
      // Fix the leak: remove references
      leakyStorage.length = 0;
      console.log("Memory cleaned: references removed!");
    });
  </script>
</body>
</html>

🧠 How This Demo Leaks Memory

  • When you click Add Leaky Element, it creates a new <div>, adds it to the DOM, then removes it.
  • But the removed element is still stored in the leakyStorage array, keeping it in memory!
  • Click this multiple times → memory usage grows unnecessarily.

✅ Clicking Clean References clears the array and allows garbage collection to free that memory.

🧪 How to Observe This

  1. Open Chrome DevTools → Memory tab.
  2. Click “Add Leaky Element” several times.
  3. Take a heap snapshot.
  4. Click “Clean References”.
  5. Take another heap snapshot.
  6. You should see the nodes are now eligible for garbage collection.

🧪 Node.js Memory Leak Example (In-Memory Cache)

📁 File: leaky-server.js

const http = require("http");

// Simulate in-memory cache (the leak)
const memoryLeakCache = {};

let counter = 0;

const server = http.createServer((req, res) => {
  if (req.url === "/leak") {
    const bigData = Buffer.alloc(5 * 1024 * 1024, "a"); // 5MB of data

    // Never deleting old data — memory grows forever
    memoryLeakCache[counter++] = bigData;

    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end(`Added 5MB data. Total keys: ${counter}\n`);
  } else if (req.url === "/status") {
    const used = process.memoryUsage();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(used, null, 2));
  } else {
    res.writeHead(404);
    res.end("Not Found");
  }
});

server.listen(3000, () => {
  console.log("Server running at http://localhost:3000");
});

▶️ How to Run This Demo

  1. Save the above code as leaky-server.js.
  2. Run it:

    node leaky-server.js
    
  3. In your browser or via curl, hit:

    • http://localhost:3000/leak repeatedly (to simulate leaks)
    • http://localhost:3000/status (to monitor memory usage)

    Example:

    curl http://localhost:3000/leak
    curl http://localhost:3000/status
    

🧠 What’s Leaking?

Every time you hit /leak, the server stores 5MB of data in memoryLeakCache without removing old entries. Over time, memory usage grows, and garbage collection can't free anything since all objects are still referenced.

✅ How to Fix It (Safe Cache Pattern)

Here’s a safe version with a cache limit:

const memorySafeCache = {};
const MAX_ENTRIES = 100;

function addToCache(key, value) {
  if (Object.keys(memorySafeCache).length >= MAX_ENTRIES) {
    const oldestKey = Object.keys(memorySafeCache)[0];
    delete memorySafeCache[oldestKey]; // clear old entries
  }

  memorySafeCache[key] = value;
}

// Or use a Map for better performance

const memorySafeCache = new Map();
const MAX_ENTRIES = 100;

function addToCache(key, value) {
  if (memorySafeCache.size >= MAX_ENTRIES) {
    const oldestKey = memorySafeCache.keys().next().value;
    memorySafeCache.delete(oldestKey);
  }
  memorySafeCache.set(key, value);
}

Or better yet, use:

  • lru-cache (a memory-safe LRU implementation)
  • external cache like Redis for larger datasets

🛠 Bonus: Monitor Memory with CLI

Use top, htop, or Node.js built-ins:

node --inspect leaky-server.js

Then open Chrome at chrome://inspect → Memory tab.


🧹 Best Practices to Prevent Memory Leaks

Here are habits you should adopt to keep your app memory-efficient:

  • ✅ Always declare variables with let, const, or var
  • ✅ Remove event listeners before removing DOM elements
  • ✅ Clear all timers/intervals before navigating away
  • ✅ Avoid holding large data in closures
  • ✅ Use WeakMap or WeakSet when storing DOM nodes or cache data

🔧 How to Detect Memory Leaks

Use browser dev tools like Chrome DevTools:

  1. Go to Performance > Memory tab
  2. Record a heap snapshot
  3. Interact with your app
  4. Take another snapshot
  5. Look for objects that should’ve been removed but still exist

You can also use tools like:

  • Lighthouse
  • Profiler tab in Chrome DevTools
  • performance.memory API

🧾 Conclusion

Memory leaks can be silent performance killers in JavaScript apps — especially SPAs, where the page never reloads. By understanding how JavaScript manages memory and following best practices like cleaning up intervals, event listeners, and DOM references, you can keep your application fast, efficient, and bug-free.

Now that you know what a memory leak is and how to fix it, you’re already ahead of many developers who only hear the term without truly understanding it.


✍️ Author’s Note

I hope this beginner-friendly guide helps you grasp the concept of memory leaks in JavaScript. If you have any questions or want to share your experiences with memory leaks, feel free to leave a comment below!


💬 Have Questions or Suggestions?

Drop a comment below or connect with me on LinkedIn or GitHub. Let’s make apps faster together! 🚀

0
Subscribe to my newsletter

Read articles from KUNTAL MAITY directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

KUNTAL MAITY
KUNTAL MAITY

I’m a passionate Full-Stack Developer who loves building performance-driven web and mobile applications. I work primarily with JavaScript, React, Next.js, Node.js, and MongoDB, and I enjoy simplifying complex concepts into developer-friendly tutorials. On this blog, you'll find hands-on guides, real-world projects, and developer insights—all aimed at helping you level up your coding skills and build production-ready apps. Whether you're into backend performance, frontend polish, or full-stack architecture, there's something here for you! Let’s learn, build, and grow together. 💻✨