JavaScript Memory Leak and Management
Learn about the principles of Garbage Collection and how to prevent memory leak in JavaScript
I remember the first time when I saw memory leak warning in the console, it was more exciting than worrying, as I’d been only hearing of the problem but never experienced it personally. And just like the saying goes: you can’t fix a problem until you see a problem. I took it as a great opportunity to understand the concept, and hence the blog today.
So first things first, what is a memory leak?
In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released. A memory leak may also happen when an object is stored in memory but cannot be accessed by the running code. — wiki
Regardless of the programming language, the memory life cycle has three phases:
- Allocate the memory;
- Use the memory
- Release the memory when not needed
The second phase is explicit in all languages while the first and last phases are explicit in low-level languages like C, but implicit in high-level languages like JavaScript. The third phase is also the stage where most memory management issues occur.
Some languages (Especially low-level ones) explicitly manage memory where the compiler is instructed when to allocate and release memory. But in JavaScript, this process is automatic, and it is known as Garbage Collection.
As per its name suggest, Garbage Collection is a process where a garbage collector (algorithm) searches and decides if a chunk of allocated memory is no longer needed and “collect” it — freeing the space.
So the key question is, how can the program decide if a piece of memory is not needed anymore?
In JavaScript, there are two ways to do so:
Reference-counting
This is the most naive garbage collection algorithm. This algorithm reduces the problem from determining whether or not an object is still needed to determining if an object still has any other objects referencing it. An object is said to be “garbage” if there are zero references pointing to it.
Downside : Circular references
The above method seems smart, but there is a limitation when it comes to circular references, aka. when two objects are created with properties that reference each other, since neither has at least one reference and can never be collected.
x.a = y; // x references y
y.a = x; // y references x
Mark-and-sweep
Due to the limitations of reference-counting, JavaScript upgraded to another mode of Garbage Collection that is been adopted in browser and Node environment today — mark and sweep. Instead of focusing on finding an object that is no longer needed, it finds an object that is unreachable.
Since an object that has zero reference is unreachable. But an object that is unreachable might still have references on it — like in the case of circular reference.
How does it do so? It has 2 (or 3 if you add the final compacting part)phases and use the concept of Heap, which is where we store variables in JavaScript and Garbage Collection takes place, and Roots (Stack Pointers), which are a type of global objects.
- Mark: Staring from roots, the collector will mark the mark bit of all objects stemming from the roots (reachable) as true (default to false)
- Sweep: The Garbage collector then traverses the heap and list those objects with mark sets to false as “Ready-to-be-freed” and free them from the memory
- Compact: When needed, all the objects after the sweeping phase will be grouped together. This step is for performance purpose mainly.
As of 2012, all modern browsers ship a mark-and-sweep garbage-collector and so is the V8 Engine used in Node.
Now understanding the mechanisms of Garbage Collection process, let’s dive into the some common mistakes causing memory leak.
Global Variables
Global variables in JavaScript are referenced by the root node ( this
), they are never garbage collected. So be careful if you accidentally assign a global variable in a local scope without declaring it first and reassigning it to null later., like the myGlobalVar
below. It can even be problematic if you assign a big chunk of data /heavy computation to a global variable explicitly.
function myGlobalVar() {
a = “super heavy memory string”;
}
So try to use ‘strict’ mode for parsing your JS code which will prevent accidental global variables. Or ‘let’ and ‘const’ for variable declaration that has a block scope.
Multiple References
This often happens with detached DOM objects when the nodes that have been removed from the DOM are still referenced by JavaScript. And since DOM is a doubly-linked tree, any reference to single node will end up with the entire tree being preserved.
For example, if we create an element through a function and assign the function call to a variable. Even the element itself is deleted later, the variable still holds reference to the node, causing it remains in memory.
function createElement() {
const div = document.createElement('div');
div.id = 'multi-ref';
return div;
}
const multiRefDiv = createElement();
document.body.appendChild(multiRefDiv);
function deleteElement() {
document.body.removeChild(document.getElementById('multi-ref'));
}
deleteElement();
But you can easily fixing it by moving the variable inside the local scope, as all variables created inside a local scope is destroyed after the function is called:
function appendElement() {
const multiRefDiv = createElement();//multiRefDiv will be deleted
document.body.appendChild(multiRefDiv);
}
appendElement();
Closures
One of the most important JavaScript features is closure. Closure is a combination of an inner function and the outer lexical environment (scope) within which that function was declared. The function will always have reference to the outer scope it resides in, and even if the function of the outer scope is executed and the outer scope is not used ever since, it will always consume a big chunk of memory as long as the inner function exists.
function outer() {
const memoryExpensiveCalc = 0 ;
return function inner() {
memoryExpensiveCalc += 1;
console.log("finish unused calc");
};
};
const check = outer();//inner uncalledfor (i = 0; i < Infinity; i++) {
check() // call inner
}
You can see that memoryExpensiveCalc
is keeping got called in inner
but is never used(returned) hence not reachable, but its value is saved in memory and its size keeps growing.
Timers and Events
This category is really an extension of the Closure category we mentioned earlier. For a classic example you can check this blog from Meteor team. But basically, with Timers like setTimeout and setInterval, and Events like addEventListener, we can accidentally create callbacks with closures holding to object references that long live in the memory.
function setCallback() {
const data = {
counter: 0,
memoryExpensiveArray: new Array(100000).join('*')
};
return function cb() {
data.counter++;
console.log(data.counter);
}
}
setInterval(setCallback(), 1000);
In the example above the data
object will never be garbage collected since there is no clean up for setInterval
, so data.memoryExpensiveArray
will always be kept in memory.
We can easily fix it by adding a reference to the callback and properly clean it up after use:
const interval= setInterval(setCallback(), 1000);
clearInterval(interval);
The same applies for using Events:
document.addEventListener(‘keyup’, listener);
document.removeEventListener(‘keyup’, listener);
So above are some common reasons for memory leak in JavaScript, also, below are some best practice with working with JavaScript objects in general, regardless of memory leak:
- Use local variables where possible but not global variables. And don’t store big objects in global variables — if you do, remember to reset them to null.
- Always use object destructuring from an object so the whole object won’t be referenced in closures.
- Copy objects where possible and avoid mutating them, you can use libraries like ImmutableJS to help yo do the work.
So that’s so much of it!
Happy Reading!