javascript

Avoiding Memory Leaks in Node.js: Best Practices for Performance

Deepu K Sasidharan

Deepu K Sasidharan on

Avoiding Memory Leaks in Node.js: Best Practices for Performance

Memory leaks are something every developer has to eventually face. They are common in most languages, even if the language automatically manages memory for you. Memory leaks can result in problems such as application slowdowns, crashes, high latency, and so on.

In this blog post, we will look at what memory leaks are and how you can avoid them in your NodeJS application. Though this is more focused on NodeJS, it should generally apply to JavaScript and TypeScript as well. Avoiding memory leaks helps your application use resources efficiently and it also has performance benefits.

On the subject of memory leaks in Node.js, you might want to explore AppSignal application monitoring for Node.js as well. It ships with a dashboard that shows the size of your heap, and the currently active top-level contexts. No instrumentation needed.

Memory Management in JavaScript

To understand memory leaks, we first need to understand how memory is managed in NodeJS. This means understanding how memory is managed by the JavaScript engine used by NodeJS. NodeJS uses the V8 Engine for JavaScript. You should check out Visualizing memory management in V8 Engine to get a better understanding of how memory is structured and utilized by JavaScript in V8.

Let's do a short recap from the above-mentioned post:

Memory is mainly categorized into Stack and Heap memory.

  • Stack: This is where static data, including method/function frames, primitive values, and pointers to objects are stored. This space is managed by the operating system (OS).
  • Heap: This is where V8 stores objects or dynamic data. This is the biggest block of memory area and it's where Garbage Collection(GC) takes place.

V8 manages the heap memory through garbage collection. In simple terms, it frees the memory used by orphan objects, i.e, objects that are no longer referenced from the Stack, directly or indirectly (via a reference in another object), to make space for new object creation.

The garbage collector in V8 is responsible for reclaiming unused memory for reuse by the V8 process. V8 garbage collectors are generational (Objects in Heap are grouped by their age and cleared at different stages). There are two stages and three different algorithms used for garbage collection by V8.

This animation explains the three step process of major GC: mark, sweep and compact

Mark-sweep-compact GC

What Are Memory Leaks

In simple terms, a memory leak is nothing but an orphan block of memory on the heap that is no longer used by the application and hasn't been returned to the OS by the garbage collector. So in effect, it's a useless block of memory. An accumulation of such blocks over time could lead to the application not having enough memory to work with or even your OS not having enough memory to allocate, leading to slowdowns and/or crashing of the application or even the OS.

What Causes Memory Leaks in JS

Automatic memory management like garbage collection in V8 aims to avoid such memory leaks, for example, circular references are no longer a concern, but could still happen due to unwanted references in the Heap and could be caused by different reasons. Some of the most common reasons are described below.

  • Global variables: Since global variables in JavaScript are referenced by the root node (window or global this), they are never garbage collected throughout the lifetime of the application, and will occupy memory as long as the application is running. This applies to any object referenced by the global variables and all their children as well. Having a large graph of objects referenced from the root can lead to a memory leak.
  • Multiple references: When the same object is referenced from multiple objects, it might lead to a memory leak when one of the references is left dangling.
  • Closures: JavaScript closures have the cool feature of memorizing its surrounding context. When a closure holds a reference to a large object in heap, it keeps the object in memory as long as the closure is in use. Which means you can easily end up in situations where a closure holding such reference can be improperly used leading to a memory leak
  • Timers & Events: The use of setTimeout, setInterval, Observers and event listeners can cause memory leaks when heavy object references are kept in their callbacks without proper handling.

Best Practices to Avoid Memory Leaks

Now that we understand what causes memory leaks, let's see how to avoid them and the best practices to use to ensure efficient memory use.

Monitor the Amount of Memory Leakage

Let's begin with making sure we monitor the amount of leakage and the size of our heap. With AppSignal that is easy, it ships with a dashboard for Heap Size:

Heap Statistics Magic Dashboard

And you can set alerts on any threshold in any graph. So you can set one to warn your team through Slack or email when the heap size is changing a lot. In this example of a dashboard, you can see the Node.js runtime reacted to a smaller amount of pages requested by making the heap smaller.

Now that we have an eye on what happens, let's look at best practices.

Reduce Use of Global Variables

Since global variables are never garbage collected, it's best to ensure you don't overuse them. Below are some ways to ensure that.

Avoid Accidental Globals

When you assign a value to an undeclared variable, JavaScript automatically hoists it as a global variable in default mode. This could be the result of a typo and could lead to a memory leak. Another way could be when assigning a variable to this, which is still a holy grail in JavaScript.

javascript
// This will be hoisted as a global variable function hello() { foo = "Message"; } // This will also become a global variable as global functions have // global `this` as the contextual `this` in non strict mode function hello() { this.foo = "Message"; }

To avoid such surprises, always write JavaScript in strict mode using the 'use strict'; annotation at the top of your JS file. In strict mode, the above will result in an error. When you use ES modules or transpilers like TypeScript or Babel, you don't need it as it's automatically enabled. In recent versions of NodeJS, you can enable strict mode globally by passing the --use_strict flag when running the node command.

javascript
"use strict"; // This will not be hoisted as global variable function hello() { foo = "Message"; // will throw runtime error } // This will not become global variable as global functions // have their own `this` in strict mode function hello() { this.foo = "Message"; }

When you use arrow functions, you also need to be mindful not to create accidental globals, and unfortunately, strict mode will not help with this. You can use the no-invalid-this rule from ESLint to avoid such cases. If you are not using ESLint, just make sure not to assign to this from global arrow functions.

javascript
// This will also become a global variable as arrow functions // do not have a contextual `this` and instead use a lexical `this` const hello = () => { this.foo = 'Message"; }

Finally, keep in mind not to bind global this to any functions using the bind or call method, as it will defeat the purpose of using strict mode and such.

Use Global Scope Sparingly

In general, it's a good practice to avoid using the global scope whenever possible and to also avoid using global variables as much as possible.

  1. As much as possible, don't use the global scope. Instead, use local scope inside functions, as those will be garbage collected and memory will be freed. If you have to use a global variable due to some constraints, set the value to null when it's no longer needed.
  2. Use global variables only for constants, cache, and reusable singletons. Don't use global variables for the convenience of avoiding passing values around. For sharing data between functions and classes, pass the values around as parameters or object attributes.
  3. Don't store big objects in the global scope. If you have to store them, make sure to nullify them when they are not needed. For cache objects, set a handler to clean them up once in a while and don't let them grow indefinitely.

Use Stack Memory Effectively

Using stack variables as much as possible helps with memory efficiency and performance as stack access is much faster than heap access. This also ensures that we don't accidentally cause memory leaks. Of course, it's not practical to only use static data. In real-world applications, we would have to use lots of objects and dynamic data. But we can follow some tricks to make better use of stack.

  1. Avoid heap object references from stack variables when possible. Also, don't keep unused variables.
  2. Destructure and use fields needed from an object or array rather than passing around entire objects/arrays to functions, closures, timers, and event handlers. This avoids keeping a reference to objects inside closures. The fields passed might mostly be primitives, which will be kept in the stack.
javascript
function outer() { const obj = { foo: 1, bar: "hello", }; const closure = () { const { foo } = obj; myFunc(foo); } } function myFunc(foo) {}

Use Heap Memory Effectively

It's not possible to avoid using heap memory in any realistic application, but we can make them more efficient by following some of these tips:

  1. Copy objects where possible instead of passing references. Pass a reference only if the object is huge and a copy operation is expensive.
  2. Avoid object mutations as much as possible. Instead, use object spread or Object.assign to copy them.
  3. Avoid creating multiple references to the same object. Instead, make a copy of the object.
  4. Use short-lived variables.
  5. Avoid creating huge object trees. If they are unavoidable, try to keep them short-lived in the local scope.

And while you are at it, make sure you monitor your heap size. Depending on the APM you use, this may need some setting up. In AppSignal APM for Node.js a heap size dashboard is set up for you automatically.

Properly Using Closures, Timers and Event Handlers

As we saw earlier, closures, timers and event handlers are other areas where memory leaks can occur. Let's start with closures as they are the most common in JavaScript code. Look at the code below from the Meteor team. This leads to a memory leak as the longStr variable is never collected and keeps growing memory. The details are explained in this blog post.

javascript
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) console.log("hi"); }; theThing = { longStr: new Array(1000000).join("*"), someMethod: function () { console.log(someMessage); }, }; }; setInterval(replaceThing, 1000);

The code above creates multiple closures, and those closures hold on to object references. The memory leak, in this case, can be fixed by nullifying originalThing at the end of the replaceThing function. Such cases can also be avoided by creating copies of the object and following the immutable approach mentioned earlier.

When it comes to timers, always remember to pass copies of objects and avoid mutations. Also, clear timers when done, using clearTimeout and clearInterval methods.

The same goes for event listeners and observers. Clear them once the job is done, don't leave event listeners running forever, especially if they are going to hold on to any object reference from the parent scope.

Conclusion

Memory leaks in JavaScript are not as big of a problem as they used to be, due to the evolution of the JS engines and improvements to the language, but if we are not careful, they can still happen and will cause performance issues and even application/OS crashes.

The first step in ensuring that our code doesn't cause memory leaks in a NodeJS application is to understand how the V8 engine handles memory. The next step is to understand what causes memory leaks. Once we understand this, we can try to avoid creating those scenarios altogether. And when we do hit memory leak/performance issues, we will know what to look for.

When it comes to NodeJS, some tools can help as well. Aside from AppSignal to monitor your Node.js production setup, Node-Memwatch and Node-Inspector are great for debugging memory issues.

References

P.S. If you liked this post, subscribe to our new JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

Deepu K Sasidharan

Deepu K Sasidharan

Our guest author Deepu K Sasidharan is the co-lead of the JHipster platform. He is a polyglot developer and Cloud-Native Advocate currently working as a Developer Advocate at Adyen. He is also a published author, conference speaker, and blogger.

All articles by Deepu K Sasidharan

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps