DEV Community

Cover image for 🚨 Memory Leaks in JavaScript & React - The Hidden Enemy
Fazal Mansuri
Fazal Mansuri

Posted on

🚨 Memory Leaks in JavaScript & React - The Hidden Enemy

Memory leaks are tricky. You don’t see them immediately. Your UI works fine… until it doesn’t.
A few minutes, hours, or days later -

❌ the app becomes slow
❌ scrolling lags
❌ typing delays
❌ CPU usage spikes
❌ mobile devices heat up or crash

And the worst part?
The bug is invisible. No errors. No warnings. Nothing in the console.

This blog will reveal exactly why memory leaks happen in JavaScript & React, how to avoid them, how to debug them, and real mistakes developers make in production.

Let’s dive in 👇


🧠 1. What Is a Memory Leak?

A memory leak happens when your program keeps holding onto memory it no longer needs, preventing the browser from freeing it.
JS uses automatic garbage collection - but GC can only help if memory becomes unreachable.

If something still references that memory → leak.


🔥 2. Why Memory Leaks Happen in JavaScript

JavaScript leaks commonly come from:

1️⃣ Global variables

window.cache = {};
Enter fullscreen mode Exit fullscreen mode

Never cleared → stored forever.

2️⃣ Closures storing unnecessary data

function heavy() {
  const big = new Array(1_000_000).fill("data");
  return () => console.log(big.length);
}
Enter fullscreen mode Exit fullscreen mode

The closure keeps big alive.

3️⃣ Event listeners not removed

window.addEventListener("scroll", handler);
// never removed → stays in memory
Enter fullscreen mode Exit fullscreen mode

4️⃣ Timers not cleared

setInterval(() => ..., 1000);
// still running even when component unmounted
Enter fullscreen mode Exit fullscreen mode

5️⃣ DOM references stored in JS

const elems = [];
elems.push(document.getElementById("box"));
// Even if 'box' is removed from DOM, elems still holds reference → leak
Enter fullscreen mode Exit fullscreen mode

⚛️ 3. Memory Leaks in React - Real Reasons They Happen

React helps prevent leaks…
…but leaks still happen when side effects run incorrectly.

Here are the most common sources:

🧩 3.1 Not cleaning up useEffect

❌ Leak example:

useEffect(() => {
  const id = setInterval(() => {
    console.log("running...");
  }, 1000);
}, []);
Enter fullscreen mode Exit fullscreen mode

After component unmount → interval continues → leak.

✅ Fix:

useEffect(() => {
  const id = setInterval(() => {
    console.log("running...");
  }, 1000);

  // Cleanup: clear interval when component unmounts
  return () => clearInterval(id);
}, []);
Enter fullscreen mode Exit fullscreen mode

🧩 3.2 Event listeners inside components

❌ Leak:

useEffect(() => {
  window.addEventListener("resize", handleResize);
}, []);
Enter fullscreen mode Exit fullscreen mode

✅ Fix:

useEffect(() => {
  window.addEventListener("resize", handleResize);

  return () => window.removeEventListener("resize", handleResize);
}, []);
Enter fullscreen mode Exit fullscreen mode

🧩 3.3 Async calls setting state after unmount

This is one of the MOST common leaks.

❌ Example:

useEffect(() => {
  fetch("/api/user")
    .then(res => res.json())
    .then(data => setUser(data)); 
}, []);
Enter fullscreen mode Exit fullscreen mode

If the user navigates away →
React tries to update state on an unmounted component → memory leak + warning.

✅ Fix:

useEffect(() => {
  const controller = new AbortController();

  fetch("/api/user", { signal: controller.signal })
    .then(res => res.json())
    .then(data => setUser(data))
    .catch(err => {
      if (err.name !== 'AbortError') {
        console.error(err);
      }
    });

  return () => controller.abort();
}, []);
Enter fullscreen mode Exit fullscreen mode

🧩 3.4 Huge arrays stored in state

React re-renders + stores stale copies.

❌ Storing large datasets in state
❌ Duplicating arrays on every update
❌ Logging huge objects inside useEffect

🔧 Fix:

  • Use pagination
  • Use memoization (useMemo) for expensive computations
  • Avoid storing raw API responses if unnecessary

🧩 3.5 Memory Leaks with React Strict Mode

React Strict Mode intentionally runs effects twice in development.
If cleanup logic is wrong → leaks appear in dev only.
⚠️ This doesn't happen in production, but fixing cleanup issues prevents real leaks.


🧪 4. Debugging Memory Leaks - Chrome DevTools (Simplest Guide)

This is the part devs struggle with.
Here is a beginner-friendly flow:

Step 1: Open Performance Monitor

Chrome → More Tools → Performance Monitor

Track:

  • JS Heap Size
  • DOM Nodes
  • Event Listeners

If these numbers keep increasing → leak.


Step 2: Take a Heap Snapshot

DevTools → Memory tab → Take Heap Snapshot

Then repeat after performing interactions.

Look for:

  • Detached HTML elements
  • Large arrays
  • Retained closures
  • Growing listeners

Step 3: Check Event Listeners

DevTools → Elements → Event Listeners

If the number keeps increasing on rerender → leak.


🛡 5. Preventing Memory Leaks - Best Practices

✔ Always clean up useEffect

✔ Avoid storing huge data in state

✔ Use paginated API responses

✔ Use AbortController to cancel fetch

✔ Throttle expensive events

✔ Use profiling tools regularly


🧨 6. Real-World Example - A Leak That Happened in Production

  1. A dashboard had multiple charts
  2. Each chart added a resize listener
  3. Rerender recreated listeners
  4. Old listeners remained
  5. 50+ listeners running simultaneously
  6. Browser froze after 10 minutes

Fix: cleanup logic in useEffect + throttling resize events.


🏁 Final Thoughts

Memory leaks are scary because you don’t notice them immediately - but the impact is real.
React apps slow down, mobile users suffer, and debugging becomes painful.

But the good news?

👉 Once you understand how leaks happen
👉 And how to avoid them
👉 You can prevent 95% of them permanently

Your UI becomes faster, cleaner, smoother - and much easier to maintain.


Top comments (0)