Memory management in JavaScript

Auteur(s) de l'article

Often overlooked in JavaScript, memory management plays an important part in the performance of our code. Let's dig into the subject and see what we need to pay attention to when writing code.
Why do we care about memory ? Our devices, while getting higher memory space with every generation, still have a limited amount of space to run our code. And today, it is expected that our code should run efficiently and with high performance.
Contrary to most low-level programming languages (C or C++ for example), memory management is automatically taken care of for us in JavaScript. However, there are some things we need to be careful to ensure we don't end up with memory leaks.

How does memory work ?

Memory is what stores our code, variables and functions that we need to run an application or website. There are two types of JavaScript engine storage:
The Stack memory stores static data, meaning data whose size is known at compile time. Stack overflow is what happens when the stack exceeds its maximum size (for example when we are stuck in an infinite loop).
The Heap memory is mainly used to store objects and functions, and the memory is dynamically allocated at runtime . A contrario from the Stack memory, the JavaScript engine does not reserved a fixed amount of memory since the size needed is unknown at compile time.
The life cycle of memory is composed of three steps:
  1. Memory allocation
  2. Use of the allocated memory
  3. Release of the allocated memory

    Memory allocation

    This happens whenever we create a variable (with the help of keywords like var, let or const) or when we call on a function.
    const name = "John Smith"; // String but also number, array, object, etc. can be allocated to variables
    
    const calculateSquare = x => x * x; // Same goes for functions
    
    const squareFour = calculateSquare(4); // Call to a function is also allocated in memory

    Use of the allocated memory

    This is the read / write actions we do with our variables and functions.

    Release of the allocated memory

    That's the step where most problems happen for memory management. It is not easy to know when a variable is not used anymore and can thus be released.
    In JavaScript, it is done automatically with the help of something called garbage collection. Its purpose is to determine whether or not a block of allocated memory can be released.

    Garbage collection

    As stated above, the main point of garbage collection is to determine when to free an allocated bit of memory automatically. The concept of reference is very important to understand how it works.
    Within the context of memory management, an object is said to reference another object if the former has access to the latter (either implicitly or explicitly). For instance, a JavaScript object has a reference to its prototype (implicit reference) and to its properties values (explicit reference).

    MDN

    Reference-counting algorithm

    It is the most "naive" implementation of garbage collection: the algorithm looks for objects which have no more references to deallocated them.
    let x = {
      a: {
        b: '',
      },
    }; 
    // New memory allocation:
    // 2 objects are created, one (a) is referenced as a property of x, the other is the assignation of the object to x
    
    let y = x; 
    // New memory allocation where y references our x variable
    // Now there are two references pointing to the x object
    
    x = 1; 
    // Now the object originally referenced by x has a unique reference pointing to y
    
    let z = y.a;
    // The object originally referenced by x has now two references, by its assignation to y and by its property a being assigned to z
    
    y = null;
    // The object originally referenced by x has now zero references. However, its property a is still referenced by z, which means it cannot yet be garbage collected.
    
    z = null;
    // Now the object doesn't have any references left, and can be garbage collected

    Mark-and-sweep algorithm

    This algorithm changes the notion for which objects need to be released, from "no longer referenced" to "unreachable".
    It starts from the roots, which are global variables being referenced in the code. In the browser, window is a root object. From there, the algorithm checks every object referenced by the roots, and each objects referenced by those objects, etc. and marks them. Anything left by the end is considered unreachable and is swept (meaning the memory space is released).
    This is the algorithm that is used in all modern browsers and on which most improvements are made nowadays.

    The most common types of memory leaks

    Global variables

    In JavaScript, whenever we assign a value to an undeclared variable, it is automatically assigned to the global object.
    foo = 'Hello World !';
    console.log(window.foo); // Hello World !
    As we saw beforehand with the mark-and-sweep algorithm, it means the foo variable will never be released from the memory space.

    Forgotten timer or callback

    In JavaScript, a common timer function is setInterval, which executes a function (callback) every x milliseconds. The objects referenced in the callback won't be released from memory until the timer is done.
    In a code like the one below, it means that the memory usage will increase every time the interval starts over, with the o.string property never being released from memory, even though there is no need for it.
    // Bad example
    const callback = () => {
      const o = {
        counter: 0,
        string: new Array(1000000).join('x'), // this property is never used, but will still be stored in memory until the setInterval finished.
      };
    
      return () => {
        o.counter++;
        console.log(o.counter);
      };
    };
    
    setInterval(callback(), 1000); // no way to stop this interval if needed
    
    // Better way
    const callback = () => {
      let counter = 0;
      let string = new Array(1000000).join('x'), // now, this will be released from the memory since it is unreferenced when the callback returns
    
      return () => {
        counter++;
        console.log(counter);
      };
    };
    
    const timer = setInterval(callback(), 1000);
    clearInterval(timer); // can be called when clicking on a button for example
    To help with debugging memory leaks, the Chrome DevTools provides a memory panel.

    Sources