Today I Learned - Rocky Kev

TIL about what causes Memory leaks in Javascript

POSTED ON:

TAGS:

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 values? If their job is done, we want them gone.

Here's an example:

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:

  1. Slowdowns: After a long session (could be hours or even a day) of working with the application, the UI becomes slower, sluggish.
  2. The web page crashes.
  3. The app pauses frequently.
  4. The JS heap ends higher than it began.
  5. 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:

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:

TIL what is npm Script

Despite their high usage they are not particularly well optimized and add about 400ms of overhead. In this article we were able to bring that down to ~22ms.

TIL fancy methods to transform Javascript Objects

You can use Object.entries(), Object.keys(), Object.fromEntries()...

TIL how to hide your JS code

ONE THING TO NOTE: Encrypting a script is stronger than obfuscation, both methods are still not adequate to protect secret content. Honestly, I don't think it's worth it on a production site, and instead just go with pure server-side if you want security. But it's fascinating.