I once debugged a mission-critical dashboard that crashed exactly every 4 hours. The culprit wasn't a complex algorithm or a backend failure—it was a simple setInterval inside a Vue component that never got cleared. In Single Page Applications (SPAs), "navigating away" doesn't mean "refreshing the page." If you don't clean up your trash, it stays in the browser's memory until the tab freezes.
A Memory Leak in Vue.js occurs when a component is unmounted (removed from the DOM), but JavaScript objects associated with it (like event listeners, timers, or third-party instances) remain referenced in the global scope, preventing the browser's Garbage Collector (GC) from reclaiming that memory.
The Zombie Guest Analogy
Concept: Imagine you rent an apartment (The Component) and invite a friend (The Event Listener) to a party. When your lease ends and you move out (Unmount), you assume your friend leaves too. But if you explicitly told them to "stay in the building lobby" (Global Scope/Window), they will stand there forever, waiting for you to come back. Do this 100 times, and the lobby becomes so crowded nobody can enter.
In technical terms, Vue handles the apartment (the DOM elements inside the template). But Vue cannot automatically handle things you attached to the "lobby" (the window, document, or body). You manually signed them in; you must manually sign them out.
The Fix: Lifecycle Management
In Vue 3, the standard approach uses the onBeforeUnmount hook. This runs before the component is destroyed, meaning you still have access to refs and DOM elements if you need to perform specific cleanup logic.
1. The "Vanilla" Implementation
The most common mistake is passing an anonymous function to addEventListener. You cannot remove an anonymous function later because the reference is lost.
// ❌ BAD PATTERN (Memory Leak)
import { onMounted } from 'vue';
onMounted(() => {
// This function is anonymous. You can never reference it again to remove it.
window.addEventListener('resize', () => {
console.log('Resizing...');
});
});
// ✅ GOOD PATTERN (Production Ready)
import { onMounted, onBeforeUnmount } from 'vue';
const handleResize = () => {
console.log('Window resized');
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
// We pass the EXACT same function reference
window.removeEventListener('resize', handleResize);
});
2. The "Pro" Implementation (VueUse)
Senior engineers rarely write manual cleanup boilerplate for standard events. We use utilities. If you are using VueUse, the useEventListener composable automatically registers the event on mount and—crucially—auto-cleans it on unmount.
// 🚀 ELITE PATTERN (VueUse)
import { useEventListener } from '@vueuse/core';
// No need for onMounted or onBeforeUnmount
// This listener is tied to the component's scope automatically
useEventListener(window, 'resize', (evt) => {
console.log(evt);
});
Watch out for: Third-party libraries like Chart.js or Leaflet. These libraries create their own DOM instances outside Vue's virtual DOM. You must call their specific .destroy() methods inside onBeforeUnmount, or the canvas elements will remain in memory forever.
3. Debugging with Chrome DevTools
How do you know if you have a leak? You look for Detached DOM Nodes.
- Open Chrome DevTools → Memory tab.
- Select Heap Snapshot.
- Click Take Snapshot (Snapshot 1).
- Interact with your app (e.g., navigate to the component and back 5 times).
- Click Take Snapshot again (Snapshot 2).
- In the filter bar, search for "Detached".
If you see Detached HTMLDivElement increasing with every snapshot, specific nodes are being kept alive by a lingering JavaScript reference (likely an event listener or a closure).
Frequently Asked Questions
Q. specific cleanup needed for KeepAlive components?
A. Yes. Components inside <KeepAlive> are not unmounted, they are hidden. onBeforeUnmount will NOT trigger when you navigate away. Instead, use onDeactivated to pause listeners/timers and onActivated to resume them.
Q. What about setTimeout and setInterval?
A. These are classic leak sources. Always assign the timer to a variable (const timer = setInterval(...)) and call clearInterval(timer) inside onBeforeUnmount.
Q. onBeforeUnmount vs onUnmounted?
A. Use onBeforeUnmount (or beforeUnmount in Options API) for almost everything. At this stage, the DOM is still intact, so you can clean up libraries attached to specific elements. onUnmounted is called after the DOM is gone, which might be too late for some third-party library cleanup methods.
Post a Comment