How to avoid memory leaks in JavaScript
Memory leaks occur when JavaScript retains references to objects that are no longer needed, preventing garbage collection and causing memory usage to grow indefinitely. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve debugged memory leaks in applications serving millions of users, reducing memory consumption from 500MB to 50MB by properly managing event listeners, timers, and closures.
The most effective approach combines proper cleanup patterns with Chrome DevTools memory profiling.
Remove Event Listeners
// Bad - listener never removed
function BadComponent() {
const button = document.querySelector('#myButton')
button.addEventListener('click', () => {
console.log('Clicked')
})
// When component unmounts, listener stays in memory
}
// Good - remove listener on cleanup
class GoodComponent {
constructor() {
this.button = document.querySelector('#myButton')
this.handleClick = this.handleClick.bind(this)
this.button.addEventListener('click', this.handleClick)
}
handleClick() {
console.log('Clicked')
}
destroy() {
this.button.removeEventListener('click', this.handleClick)
}
}
// Best - AbortController for automatic cleanup
function BestComponent() {
const button = document.querySelector('#myButton')
const controller = new AbortController()
button.addEventListener('click', () => {
console.log('Clicked')
}, { signal: controller.signal })
// Cleanup all listeners at once
return {
destroy: () => controller.abort()
}
}
Clear Timers and Intervals
// Bad - timer keeps running after component removed
function BadTimer() {
setInterval(() => {
console.log('Tick')
updateUI()
}, 1000)
}
// Good - clear timer on cleanup
class GoodTimer {
constructor() {
this.timerId = setInterval(() => {
console.log('Tick')
this.updateUI()
}, 1000)
}
updateUI() {
// Update logic
}
destroy() {
clearInterval(this.timerId)
}
}
// React example
function TimerComponent() {
useEffect(() => {
const timerId = setInterval(() => {
console.log('Tick')
}, 1000)
// Cleanup function
return () => clearInterval(timerId)
}, [])
return <div>Timer running</div>
}
Avoid Closures Retaining Large Objects
// Bad - closure holds entire data array in memory
function processData(data) {
// data is huge array
const hugeArray = data.map(item => ({ ...item, processed: true }))
// This closure keeps hugeArray in memory forever
return function() {
console.log(`Processed ${hugeArray.length} items`)
}
}
// Good - extract only needed data
function processData(data) {
const hugeArray = data.map(item => ({ ...item, processed: true }))
const count = hugeArray.length // Extract only what's needed
// Closure only holds count, not entire array
return function() {
console.log(`Processed ${count} items`)
}
}
// Better - no closure at all
function processData(data) {
const hugeArray = data.map(item => ({ ...item, processed: true }))
const count = hugeArray.length
return { count, summary: `Processed ${count} items` }
}
Clear DOM References
// Bad - keeps DOM nodes in memory
class BadCache {
constructor() {
this.cache = {}
}
store(id, element) {
this.cache[id] = element // Holds DOM reference
}
get(id) {
return this.cache[id]
}
}
// When element removed from DOM, it's still in cache
// Good - use WeakMap for auto cleanup
class GoodCache {
constructor() {
this.cache = new WeakMap()
}
store(element, data) {
this.cache.set(element, data)
// When element is removed from DOM and no other references exist,
// WeakMap entry is automatically garbage collected
}
get(element) {
return this.cache.get(element)
}
}
// Usage
const cache = new GoodCache()
const button = document.querySelector('#myButton')
cache.store(button, { clicks: 0 })
// When button removed, cache entry auto-cleans
button.remove()
Clean Up Observers
// Bad - observer never disconnected
function BadObserver() {
const observer = new MutationObserver(mutations => {
console.log('DOM changed')
})
observer.observe(document.body, { childList: true })
// Never disconnected - keeps observing forever
}
// Good - disconnect on cleanup
class GoodObserver {
constructor() {
this.observer = new MutationObserver(mutations => {
console.log('DOM changed')
})
this.observer.observe(document.body, { childList: true })
}
destroy() {
this.observer.disconnect()
}
}
// IntersectionObserver example
class LazyLoader {
constructor() {
this.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target)
this.observer.unobserve(entry.target)
}
})
})
}
observe(element) {
this.observer.observe(element)
}
destroy() {
this.observer.disconnect()
}
loadImage(img) {
img.src = img.dataset.src
}
}
Break Circular References
// Bad - circular references prevent GC
function createCircular() {
const obj1 = { name: 'obj1' }
const obj2 = { name: 'obj2' }
obj1.ref = obj2
obj2.ref = obj1
return obj1
// Both objects kept in memory due to circular refs
}
// Good - break references on cleanup
class Node {
constructor(value) {
this.value = value
this.next = null
}
destroy() {
// Break circular references
let current = this
while (current) {
const next = current.next
current.next = null
current = next
}
}
}
// Usage
const head = new Node(1)
head.next = new Node(2)
head.next.next = new Node(3)
// Cleanup
head.destroy()
Avoid Global Variables
// Bad - pollutes global scope
var cache = []
var userData = {}
function addData(data) {
cache.push(data)
userData[data.id] = data
// These grow indefinitely
}
// Good - scoped with cleanup
class DataManager {
constructor() {
this.cache = []
this.userData = new Map()
}
addData(data) {
this.cache.push(data)
this.userData.set(data.id, data)
}
clear() {
this.cache = []
this.userData.clear()
}
destroy() {
this.clear()
this.cache = null
this.userData = null
}
}
// Usage
const manager = new DataManager()
manager.addData({ id: 1, name: 'Test' })
// Cleanup
manager.destroy()
Detect Memory Leaks with Chrome DevTools
// 1. Open Chrome DevTools > Memory tab
// 2. Take heap snapshot
// 3. Perform actions that might leak
// 4. Take another snapshot
// 5. Compare snapshots
// Example: Test for leaks
class ComponentTester {
constructor() {
this.instances = []
}
createInstances(count) {
for (let i = 0; i < count; i++) {
this.instances.push(new MyComponent())
}
}
destroyInstances() {
this.instances.forEach(instance => instance.destroy())
this.instances = []
}
}
// Test procedure:
// 1. Take baseline snapshot
// 2. Create 100 components
// 3. Take snapshot (memory should increase)
// 4. Destroy all components
// 5. Force GC (trash icon in DevTools)
// 6. Take final snapshot
// 7. Memory should return to baseline
// If memory doesn't return, there's a leak
Profile Memory Usage
// Use performance.memory (Chrome only)
if (performance.memory) {
console.log('Used JS Heap:', performance.memory.usedJSHeapSize)
console.log('Total JS Heap:', performance.memory.totalJSHeapSize)
console.log('Heap Limit:', performance.memory.jsHeapSizeLimit)
}
// Monitor memory over time
class MemoryMonitor {
constructor() {
this.samples = []
this.intervalId = null
}
start(intervalMs = 1000) {
this.intervalId = setInterval(() => {
if (performance.memory) {
this.samples.push({
timestamp: Date.now(),
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize
})
}
}, intervalMs)
}
stop() {
clearInterval(this.intervalId)
}
getReport() {
const first = this.samples[0]
const last = this.samples[this.samples.length - 1]
const growth = last.used - first.used
const duration = last.timestamp - first.timestamp
return {
duration,
initialMemory: first.used,
finalMemory: last.used,
growth,
growthRate: growth / (duration / 1000) // bytes per second
}
}
}
// Usage
const monitor = new MemoryMonitor()
monitor.start()
// Run your app...
setTimeout(() => {
monitor.stop()
console.log(monitor.getReport())
}, 60000)
Best Practice Note
This is how we prevent memory leaks across all CoreUI JavaScript applications. Memory leaks gradually degrade performance and can crash applications running for extended periods. Always remove event listeners when components unmount, clear timers and intervals, avoid closures that retain large objects, use WeakMap for caching DOM references, disconnect observers when done, break circular references, and regularly profile memory usage in Chrome DevTools. In React, use cleanup functions in useEffect; in vanilla JS, implement destroy methods for all components.
For production applications, consider using CoreUI’s Admin Templates which include memory-safe component patterns.
Related Articles
For related performance optimization, check out how to optimize array operations in JavaScript and how to profile JavaScript performance.



