Memory Leaks in JavaScript and how to avoid them.

Eduard Hayrapetyan
Preezma Software Development Company
5 min readJul 26, 2020

Overview

We just need to remember that memory is limited , so when it comes to call stack and memory heap ,those are two places that JavaScript remembers or stores memory. And we have limited use of them. So we will do our best to write efficient code to avoid the stack overflow or a memory leak and to manage memory well.

What are memory leaks?

A Memory leak can be defined as a piece of memory that is no longer being used or required by an application but for some reason is not returned back to the OS.In simple terms it is forgotten data forever waiting to be used.Leaks are the cause of whole class of problems: slowdowns, crashes, high latency, and even problems with other applications.

So lets create our own memory leak 🥴 the easiest way to do this is the following example

let arr = [];
for(let i = 5; i > 1; i++){
arr.push(i-1) // Warning! do not try this at home )
}

So we are going to run an infinite loop that keeps pushing to the array i -1 over and over until we will fill up our memory and there’s nothing left for us to use. In this case garbage collection wasn’t really working because we had this array and we are using this array over and over until we crashed the program. So most likely you are not going to do this in your programs, so lets dive and see what common memory leaks can happen in our application.

The four types of common JavaScript leaks

1.Undeclared/ Global Variables

One of the ways in which JavaScript is permissive is in the way it handles undeclared variables: a reference to an undeclared variable creates a new variable inside the global object.

In the case of browsers, the global object is a “window”. For instance:

function foo(arg){
bar = "this is a hidden global variable";
}

But, is in fact

function foo(arg){
window.bar = "this is an explicit global variable";
}

Always avoid using global variables or prevent these mistakes from happening, add 'use strict'; at the beginning of your JavaScript files. This enables a stricter mode of parsing JavaScript that prevents accidental globals.

2.Forgotten timers or callbacks

Having a setTimeout or a setInterval referencing some object in the callback is the most common way of preventing the object from being garbage collected. If we set the recurring timer in our code the reference to the object from the timer's callback will stay active for as long as the callback is invocable.

In the example below the data object can be garbage collected only after the timer is cleared. Since we have no reference to setInterval, it can never be cleared and data.hugeString is kept in memory until the app stops, although never used.

function setCallback() {    
const data = {
counter: 0,
hugeString: new Array(100000).join('x')
};
return function cb() {
data.counter++; // data object is now part of the callback's scope console.log(data.counter);
}
}
setInterval(setCallback(), 1000); // how do we stop it?

How to prevent it: Especially if the callback’s lifespan is undefined or indefinite:

  • being aware of the objects referenced from the timer’s callback,
  • using the handle returned from the timer to cancel it when necessary.
  • returned from the timer to cancel it when necessary.
function setCallback() {
// 'unpacking' the data object
let counter = 0;
const hugeString = new Array(100000).join('x'); // gets removed when the setCallback returns
return function cb() {
counter++; // only counter is part of the callback's scope
console.log(counter);
}
}
const timerId = setInterval(setCallback(), 1000); // saving the interval ID// doing something ...clearInterval(timerId); // stopping the timer i.e. if button pressed

3.Out of DOM references (Event listeners)

Active event listener will prevent all variables captured in its scope from being garbage collected. Once added, the event listener will remain in effect until:

  • explicitly removed with removeEventListener()
  • the associated DOM element is removed.

A DOM element belongs to the DOM, but it also exists in the Object Graph Memory. Therefore, if you delete the former, you should else delete the latter.

var trigger = document.getElementById("trigger");
var elem = document.getElementById("elementToDelete");
trigger.addEventListener("click", function(){
elem.remove();
});

In this example, after you click trigger, elementToDelete is removed from the DOM. But since it’s still referenced within the listener, the allocated memory for the object is still used.

How to prevent it: We should always unregister the event listener once no longer needed, by creating a reference pointing to it and passing it to removeEventListener() or addEventListener() can take a third parameter, which is an object providing additional options. Given that {once: true} is passed as a third parameter to addEventListener(), the listener function will be automatically removed after handling the event once.

trigger.addEventListener("click", function(){
elem.remove();
},{once: true})); // listener will be removed after running once

4.Closures

Function-scoped variables will be cleaned up after the function has exited the call stack and if there aren’t any references left outside of the function pointing at them. The closure will keep the variables referenced and alive although the function has finished executing and its execution context and variable environment are long gone.

function outer() { 
const potentiallyHugeArray = [];
return function inner() {
potentiallyHugeArray.push('Hello');
console.log('Hello');
};
};
const sayHello = outer(); // contains definition of the function inner
function repeat(fn, num) {
for (let i = 0; i < num; i++){
fn();
}
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray
// now imagine repeat(sayHello, 100000)

In this example, potentiallyHugeArray is never returned from any of the functions and cannot be reached, but its size can grow infinitely depending on how many times we call the function inner().

How to prevent it: Closures are an unavoidable and an integral part of JavaScript, so it is important to:

  • understand when the closure has been created and what objects are retained by it,
  • understand closure’s expected lifespan and usage (especially if used as a callback).

Conclusion

Integral part of the memory management process is understanding the typical memory leaks sources to prevent them from happening in the first place. In the end, when it comes to memory and performance, it is the user experience that is at stake and that’s what matters most.
If you ❤️ this article, click the clap 👏 , I hope this article was helpful for you.

--

--