TIL about what causes Memory leaks in Javascript
POSTED ON:
TAGS: javascript advanced-js memory
What is memory and Garbage Collector #
In JavaScript, memory is automatically allocated each time you create, for instance, an object, an array, a string, or a DOM element.
The browser keeps objects in heap memory while they can be reached from the root through the reference chain.
Garbage Collector is a background process in the JavaScript engine that identifies unreachable objects, removes them, and reclaims the underlying memory. In other words: "As long as it is possible to reach it, keep it".
image via Beyond Memory Leaks in JavaScript
In the image, notice that the red dotted function->value
is not connected to anything. That will be garbage collected because it's not being used. That's what we want! But what about all those grey value
s? If their job is done, we want them gone.
Here's an example:
- create element (el)
- create a new function that references that element
- set the function to be the onclick of that element
- overwrite the element with a new element
via: StackOverflow
The issue here is that there's a reference to that element and it's function, but it'll never ever go away because in Javascript, it'll 'assume' your new function will still be referenced.
Then why create memory leaks? #
Nobody purposely 'creates' memory leaks.
This accidental memory leak:
// this
function accidentalGlobal(arg) {
global = "this is a hidden global variable";
}
// the same as this
function accidentalGlobal(arg) {
window.global = "this is a hidden global variable";
}
still works. It's valid. No errors.
However, if your app is running slowly or even crashing unexpectedly, that’s the first clue that you may have a memory leak.
Some more common patterns:
- Slowdowns: After a long session (could be hours or even a day) of working with the application, the UI becomes slower, sluggish.
- The web page crashes.
- The app pauses frequently.
- The JS heap ends higher than it began.
- You see an increasing node size and/or listeners size.
The more common reasons #
1. Closure
Closures 'hide' values.
They're actually very excellent for optimization! But they can also be a source for memory leaks if not used correctly.
var newElem;
function outer() {
var someText = new Array(1000000);
var elem = newElem;
// never called. But still references 'elem'
function inner() {
if (elem) return someText;
}
return function () {};
}
setInterval(function () {
newElem = outer();
}, 5);
In the above example, function inner is never called but keeps a reference to elem. But as all inner functions in a closure share the same context, inner(line 7) shares the same context as function(){} (line 12)which is returned by outer function. Now in every 5ms we make a function call to outer and assign its new value(after each call) to newElem which is a global variable. As long a reference is pointing to this function(){}, the shared scope/context is preserved and someText is kept because it is part of the inner function even if inner function is never called. Each time we call outer we save the previous function(){} in elem of the new function. Therefore again the previous shared scope/context has to be kept. So in the nth call of outer function, someText of the (n-1)th call of outer cannot be garbage collected. This process continues until your system runs out of memory eventually.
via Lambdatest
2. setTimeout/setInterval
Often setTimeout/setInterval have a another call to stop them. When they're hidden, you can't target them directly.
3. Lingering Dom References
// creates memory leak
// element is removed, but still referenced!
const trigger = document.getElementbyId('trigger');
const element = document.getElementbyId('elementToDelete');
trigger.addEventListener("click", () => {
element.remove();
});
// optimized version
// element is assigned, but also remove in one go
const trigger = document.getElementbyId('trigger');
trigger.addEventListener("click", () => {
const element = document.getElementbyId('elementToDelete');
element.remove();
});
4. lingering EventListeners
An event listener prevents objects and variables captured in its scope from being garbage collected. Thus, if you forget to stop listening, you can expect a leak in memory.
addEventListener
is the most common way in JavaScript to add an event listener, which will remain active until:
- you explicitly remove it with removeEventListener(), or
- the associated DOM element will be removed.
function doSomething() {
// ...
}
document.addEventListener('keydown', doSomething); // add listener
document.removeEventListener('keydown', doSomething); // remove listener
We can't remove document
.
That means you’ll be stuck with the doSomething()
listener and whatever it keeps in its scope if you won’t clean up by calling removeEventListener()
.
In case you need to fire your event listener only once, you can add a third parameter {once: true}
to addEventListener()
, so that the listener function will be automatically removed after performing its job:
5. Global variables
Our accidental global variable example above!
Anything assigned to the global is never garbaged collected.
Global stores (like Redux) are also global variables! it will never get cleaned up.
A way to 'manually' clean it up is by nulling
them or reassign them.
REFERENCES #
Causes of Memory Leaks in JavaScript and How to Avoid Them
How to Identify, Diagnose, and Fix Memory Leaks in Web Apps
How to escape from memory leaks in JavaScript
Related TILs
Tagged: javascript