How to fix memory leaks in Vue
Memory leaks occur when components retain references to objects after unmounting, causing memory consumption to grow and application performance to degrade over time. As the creator of CoreUI, a widely used open-source UI library, I’ve debugged and prevented memory leaks in Vue applications throughout my 11 years of frontend development. The most systematic approach is properly cleaning up event listeners, timers, watchers, and subscriptions in onBeforeUnmount lifecycle hook. This method ensures components release resources when destroyed, preventing memory accumulation during navigation and preventing browser slowdowns in long-running applications.
Clean up event listeners, intervals, watchers, and subscriptions in onBeforeUnmount to prevent memory leaks.
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
// Potential memory leak: Global event listener
const windowWidth = ref(window.innerWidth)
// BAD: Without cleanup
onMounted(() => {
window.addEventListener('resize', handleResize)
})
// GOOD: With cleanup
let resizeHandler
onMounted(() => {
resizeHandler = () => {
windowWidth.value = window.innerWidth
}
window.addEventListener('resize', resizeHandler)
})
onBeforeUnmount(() => {
// Clean up event listener
window.removeEventListener('resize', resizeHandler)
})
// Potential memory leak: Timer
const counter = ref(0)
let intervalId
onMounted(() => {
// BAD: Interval continues after component unmounts
intervalId = setInterval(() => {
counter.value++
}, 1000)
})
onBeforeUnmount(() => {
// GOOD: Clear interval
if (intervalId) {
clearInterval(intervalId)
}
})
// Potential memory leak: Watcher
const searchQuery = ref('')
let unwatch
onMounted(() => {
// Watchers created with watch() return stop function
unwatch = watch(searchQuery, (newValue) => {
console.log('Search query changed:', newValue)
})
})
onBeforeUnmount(() => {
// GOOD: Stop watcher
if (unwatch) {
unwatch()
}
})
</script>
Common memory leak patterns and fixes:
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import EventBus from './eventBus'
import axios from 'axios'
// 1. Event Bus listeners
const message = ref('')
onMounted(() => {
// Subscribe to event bus
EventBus.on('notification', handleNotification)
})
function handleNotification(data) {
message.value = data.message
}
onBeforeUnmount(() => {
// IMPORTANT: Unsubscribe from event bus
EventBus.off('notification', handleNotification)
})
// 2. API request cancellation
let abortController
async function fetchData() {
// Create new AbortController for each request
abortController = new AbortController()
try {
const response = await axios.get('/api/data', {
signal: abortController.signal
})
return response.data
} catch (error) {
if (axios.isCancel(error)) {
console.log('Request cancelled')
}
throw error
}
}
onBeforeUnmount(() => {
// Cancel pending requests
if (abortController) {
abortController.abort()
}
})
// 3. Third-party library cleanup
let chartInstance
onMounted(() => {
// Initialize chart library
chartInstance = new Chart(document.getElementById('chart'), {
type: 'line',
data: chartData
})
})
onBeforeUnmount(() => {
// Destroy chart instance
if (chartInstance) {
chartInstance.destroy()
chartInstance = null
}
})
// 4. DOM element references
const elementRef = ref(null)
let observer
onMounted(() => {
// Create IntersectionObserver
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible')
}
})
})
if (elementRef.value) {
observer.observe(elementRef.value)
}
})
onBeforeUnmount(() => {
// Disconnect observer
if (observer) {
observer.disconnect()
observer = null
}
})
// 5. WebSocket connections
let ws
onMounted(() => {
ws = new WebSocket('ws://localhost:8080')
ws.onmessage = (event) => {
console.log('Message received:', event.data)
}
})
onBeforeUnmount(() => {
// Close WebSocket connection
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close()
}
})
</script>
<template>
<div>
<div ref='elementRef'>Observed element</div>
<p>{{ message }}</p>
<p>Counter: {{ counter }}</p>
<canvas id='chart'></canvas>
</div>
</template>
Detecting memory leaks with Chrome DevTools:
<script setup>
// Test component for memory leak detection
import { ref, onMounted, onBeforeUnmount } from 'vue'
const data = ref([])
let intervalId
onMounted(() => {
// Simulate memory leak
intervalId = setInterval(() => {
// Continuously adding data without cleanup
data.value.push({
id: Date.now(),
content: 'x'.repeat(10000) // Large string
})
}, 100)
})
// MISSING: No cleanup causes memory leak
// onBeforeUnmount(() => {
// clearInterval(intervalId)
// data.value = []
// })
</script>
Steps to detect memory leaks:
- Open Chrome DevTools
- Go to Memory tab
- Take heap snapshot
- Navigate to component and back multiple times
- Take another heap snapshot
- Compare snapshots - component instances should be garbage collected
- If detached DOM nodes or component instances remain, investigate
Composable for automatic cleanup:
// useCleanup.js
import { onBeforeUnmount } from 'vue'
export function useCleanup() {
const cleanupTasks = []
function addCleanup(fn) {
cleanupTasks.push(fn)
}
function addInterval(callback, delay) {
const id = setInterval(callback, delay)
addCleanup(() => clearInterval(id))
return id
}
function addTimeout(callback, delay) {
const id = setTimeout(callback, delay)
addCleanup(() => clearTimeout(id))
return id
}
function addEventListener(target, event, handler, options) {
target.addEventListener(event, handler, options)
addCleanup(() => target.removeEventListener(event, handler, options))
}
onBeforeUnmount(() => {
cleanupTasks.forEach(fn => fn())
})
return {
addCleanup,
addInterval,
addTimeout,
addEventListener
}
}
Using cleanup composable:
<script setup>
import { ref } from 'vue'
import { useCleanup } from './useCleanup'
const cleanup = useCleanup()
const count = ref(0)
// Automatically cleaned up
cleanup.addInterval(() => {
count.value++
}, 1000)
cleanup.addEventListener(window, 'resize', () => {
console.log('Window resized')
})
cleanup.addCleanup(() => {
console.log('Custom cleanup logic')
})
</script>
Here the onBeforeUnmount lifecycle hook executes cleanup functions before component destruction. Event listeners must be removed with removeEventListener using same handler reference. Timers require clearInterval or clearTimeout with timer ID to stop execution. Watchers return stop function that should be called to prevent watcher execution. Third-party libraries often provide destroy or dispose methods for cleanup. AbortController cancels pending HTTP requests when component unmounts. The useCleanup composable centralizes cleanup logic for reusability across components.
Best Practice Note:
This is the memory leak prevention strategy we implement in CoreUI Vue components to ensure stable performance in long-running applications. Use Chrome DevTools Memory profiler regularly during development to catch memory leaks early, implement strict cleanup patterns for all subscriptions and listeners, leverage Vue DevTools to track component lifecycle and detect components not being destroyed, and consider using weakRef or weakMap for caching when strong references might cause leaks.



