JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
137: Memory Leaks and How to Avoid Them
Most of us treat JavaScript's garbage collector like a magic butler—we just throw things away and assume they disappear. For the most part, that works. But the garbage collector only cleans up memory that is "unreachable." The moment you accidentally keep a reference to a piece of data that you no longer need, you've created a memory leak. In a small script, you won't notice. In a long-running Single Page Application (SPA), these leaks accumulate until the browser tab slows to a crawl or crashes entirely.
The 'Set It and Forget It' Trap
I see this most often with global event listeners. Imagine you're building a custom notification toast that needs to reposition itself whenever the browser window is resized. The naive way to handle this is to just attach a listener to the window the moment the toast is created.
function createNotification(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerText = message;
document.body.appendChild(toast);
const reposition = () => {
console.log('Repositioning toast...');
toast.style.top = `${window.scrollY}px`;
};
// The leak starts here
window.addEventListener('resize', reposition);
// We provide a way to remove the toast from the UI
toast.onclick = () => {
toast.remove();
};
}
On the surface, this looks fine. When the user clicks the toast, it's removed from the DOM. You might think, "Great, the toast is gone, the memory is reclaimed." But here's the problem: the window object still holds a reference to the reposition function, and that function holds a reference to the toast element via its closure. Even though the toast isn't in the DOM anymore, it's still sitting in memory because the window is still listening for a resize event to trigger a function that needs that toast.
Why the Garbage Collector Can't Help You Here
The garbage collector is essentially looking for a path from the "root" (the global object) to your data. In the example above, the path is: window → event listeners list → reposition function → toast element. As long as that path exists, that div and all its associated data are "reachable," so the GC is forbidden from touching them. If your app creates and destroys dozens of these notifications over a session, you're effectively leaking DOM nodes and memory every single time.
Taking Control of the Lifecycle
To fix this, you have to be explicit. You cannot trust the environment to know when a piece of logic is no longer relevant. The professional approach is to ensure every "setup" has a corresponding "teardown."
function createNotification(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerText = message;
document.body.appendChild(toast);
const reposition = () => {
toast.style.top = `${window.scrollY}px`;
};
window.addEventListener('resize', reposition);
toast.onclick = () => {
// Clean up the listener BEFORE removing the element
window.removeEventListener('resize', reposition);
toast.remove();
};
}
Now, when the toast is clicked, we sever the link between the global window and our local function. The path is broken, the reposition function becomes unreachable, and the toast element can finally be swept away. If you're working in a modern framework, this is exactly why you have lifecycle hooks like useEffect cleanup functions or onDestroy methods.
There is a slight trade-off here: you're writing more code and you have to be disciplined. It's easy to forget to remove one listener in a complex component. However, the alternative is a "zombie" application that gets slower the longer a user keeps it open. I'd much rather spend an extra ten seconds writing a removeEventListener call than spend three days debugging a vague "out of memory" report from a user in production.
📋 Practical Task
Fixing the Zombie Stock Ticker
You are maintaining a dashboard that has a "Live Stock Ticker" widget. The widget uses setInterval to fetch new prices every second. However, the developers noticed that even after the ticker is closed, the network requests keep happening in the background, and memory usage continues to climb.
Your Task: Modify the closeTicker function in the code below to properly stop the timer and prevent the memory leak.
const stockTicker = {
timerId: null,
uiElement: null,
start: function() {
this.uiElement = document.createElement('div');
this.uiElement.innerText = 'Fetching prices...';
document.body.appendChild(this.uiElement);
// This interval creates a closure that keeps the ticker object alive
this.timerId = setInterval(() => {
console.log('Fetching latest stock prices...');
this.uiElement.innerText = `Price: $${(Math.random() * 100).toFixed(2)}`;
}, 1000);
},
closeTicker: function() {
// TODO: Fix the memory leak here
this.uiElement.remove();
this.uiElement = null;
}
};
// Simulate usage
stockTicker.start();
setTimeout(() => {
stockTicker.closeTicker();
console.log('Ticker closed. If you still see "Fetching latest stock prices..." in the console, the leak is still there!');
}, 3500);
There are no comments for now.