Javascript Memory Leaks
Memory leaks are often associated with big enterprise RESTful services when an object is stored in memory but cannot be accessed by any of the running code. This causes diminishing performance ability of the application by reducing the amount of available memory for it to perform tasks (and could eventually lead to thrashing).
Since Javascript has normally been used for client side scripting, this has not been an issue because when a user refreshes a page or closes the browser the information about the entire page is removed from memory (obviously excluding cookies, browser storage capabilities, or etc.).
However since Javascript is more and more being used to create web applications using SPA architecture, AJAX calls, and other tools to provide a user experience similar to a desktop application, we start encountering (relatively minor) memory leak problems. I mean the worst thing is that a production user has to refresh the page right?
Mark-and-Sweep Garbage Collection #
While older browsers like IE6 and IE7 uses a reference-counting GC to handle circular references (DOM elements or parent-child scenarios), all modern browsers make use of the mark-and-sweep GC algorithm since 2012.
The MaS GC algorithm generally does the following:
- Determine list of the GC-roots (Examples include: the
window
object, built-in object maps, symbol table, stacks of VM threads, compilation cache, handle scopes, and global handles; among many others). - Mark all the objects, which are reachable from this GC-roots as available.
- Clean up all other non-reachable objects (used to be objects that are “not needed”).
Common Memory Leak Sources: #
- Document Object Model (DOM)
- Closures
- Events and Pub/Sub Pattern
- Misuse of Libraries and Frameworks
DOM #
Since the DOM is a doubly-linked tree, having reference to any node in such a tree will retain entire tree from garbage collection. Think about an example of saving a DOM element in javascript and then later deleting that said element (or it’s parent/s element) but not the variable holding on to it. This causes a Detached DOM which holds reference to not only the said DOM element but the entire tree (since it’s doubly-linked).
Solution: Determine which parts of the Detached DOM from the heap snapshot are not needed any more and remove references to them.
Closure #
Did you ever realize that when you don’t put a reference to an object (even a simple console.log
), you won’t have access to it when you randomly drop a debugger
or a breakpoint on that function you return even if it is under closure? It’s because that object has been garbage collected and was removed from memory, so having a reference will let you have access to it.
But what if you use eval() to evaluate javascript code represented as a string in your callback? Yes - that’s not really something you would do but hear me out. Now the GC marker will, because of closure, not mark any of the objects under closure and the said objects will remain in the application. So when the eval
function needs it, it is there.
This can be replicated in a smaller case when using calls such as function.bind(this)
and passing functions along to be executed later, these contexts will then be held in memory until the function is let go.
Solution: Closures actually work quite well (as demonstrated by dropping a breakpoint and not getting a reference to something not used). It’s more of a thing to keep in mind especially as we go to the third case of Events and Pub/Sub Pattern.
Events and Pub/Sub Pattern #
The Pub/Sub pattern is the breakout pattern of choice of the self-reliant components using the Observer and Singleton pattern. As a result, there exists an Object that lives throughout the application which contains event listeners. Subscribers that fail to unsubscribe however would result in memory leaks.
Solution: a removeSubscriber()
call must be present on each subscriber (by the publisher) when it is done with the subscriber. Checking with Chrome Dev Tools’ timeline can isolate these problems by finding out which functions keeps progressively increasing the number of listeners.
Libraries and Frameworks #
While you can delete all references you can think of to an object, the framework or library of choice would often still have their own references to it. In the example below, while you may have removed the element with id="harry"
from the DOM, jQuery still retains a reference to it so that it could do the $.remove()
function later on. You’d pretty much have to buy in to the framework or library of choice.
(() => {
$('#harry').on('click', () => {
console.log('potter!');
});
// Memory Leak
const wizard = document.querySelector('#harry');
wizard.parentNode.removeChild(wizard);
// No Memory Leak
// $('#harry').remove();
})();
Solution: Write vanilla javascript with minimal tooling (jk, but those who know me will know that would be my preferred answer). If you do have to go with the framework route, it’s a bit harder to debug since you’d have to find where you’re leaking and supply the proper clean up methods on the $destroy or off or whatnot.