What is a memory leak?
A memory leak is a phenomenon where memory is reserved but upon process completion is never
cleared, thus causing lingering/dangling
references to memory which should have been freed up.
These leaks can lead to increased memory usage and potential performance issues.
Symptoms of a memory leak
Memory leaks in JavaScript can be tricky to identify, but they typically exhibit some common symptoms. Here are some signs that might indicate a memory leak:
- Increasing Memory Usage Over Time: One of the most apparent symptoms of a memory leak is a continuous increase in memory usage as your JavaScript application runs. If the memory consumption steadily grows without stabilizing, it could be an indication of memory leaks.
- Sluggish Performance or Slow Response: As memory usage increases, the application's performance may degrade. You might notice that the application becomes slower, and user interactions become less responsive, even for relatively simple tasks.
- Longer Garbage Collection
(GC)
Cycles: Memory leaks can prolong the time taken by garbage collection cycles. Garbage collection is responsible for freeing up unused memory, but with memory leaks, theGC
has more work to do, leading to longer pauses in your application. - Frequent Page Crashes or Unresponsive Pages: In extreme cases of memory leaks, the application may crash due to running out of memory. Alternatively, it might become unresponsive, forcing the user to reload the page.
- Continuous Increase in Network Requests: Memory leaks might cause your application to request the same resources repeatedly, thinking they are new, leading to unnecessary network usage.
- Persistent Event Listeners: Event listeners that are not removed properly can be a sign of memory leaks, especially if they reference objects that should have been released.
- Resource or Handle Leaks: If your application uses external resources like files or network connections, you might notice these resources not being released properly, leading to resource exhaustion.
- Crashes in Specific Scenarios: Memory leaks might not always be evident in general usage but may cause crashes or errors in specific scenarios or edge cases.
To identify memory leaks, you can use various browser development tools like Chrome DevTools
or Firefox Developer Tools. These tools offer memory profiling features that allow you to monitor memory usage, analyze heap snapshots, and identify potential memory leaks in your JavaScript application. Regularly testing your application's memory usage and conducting heap analyses can help you catch and fix memory leaks before they become significant problems.
Investigation via Chrome inspect
-
Run your
Nodejs
application via this commandnode --inspect <path to js file>
for examplenode --inspect src\index.js
-
Afterwards open your
Chrome
browser, and visitchrome://inspect/#devices
URL -
Your running application should appear like above, click on the
inspect
link would open below window -
Click on
start
and start stressing your application such that it starts utilizing memory, do it for like 20 - 30 minutes (could be more dependingon the exact scenario) and afterwards when you see that memory usage is risen significantly, click on
stop
That should open the below window
-
Above you can see
each function call
and it’smemory utilization
which can help you tremendously when identifying which function is actually absorbing all the memoryand is not getting garbage collected.
Some common code snippets which can cause significant memory leaks
-
Accidental Global Variables: Unintentionally declaring variables in the global scope can cause memory leaks as they won't be garbage collected until the page is refreshed or closed.
function foo() { // Accidental global variable assignment myVar = 'leaked data'; }
-
Closures: Closures can inadvertently retain references to variables and prevent them from being garbage collected.
function createClosure() { var data = 'sensitive information'; return function() { // Closure holds a reference to 'data' console.log(data); }; } var leakyFunction = createClosure();
-
Timers and Intervals: Not clearing intervals or timeouts can keep the associated functions and closures in memory even after they are no longer needed.
var intervalId = setInterval(function() { // Some repeated task }, 1000); // Missing: clearInterval(intervalId);
-
Caching: Caching data excessively can lead to memory leaks, especially if the cached data is not cleared when no longer needed.
var cache = {}; function fetchData(url) { if (cache[url]) { return cache[url]; } else { // Fetch data and store it in 'cache' } }
-
Large Data Structures: Holding onto large data structures unnecessarily can consume significant memory.
var largeData = [...Array(1000000).keys()]; // Large data is no longer needed but still in memory
-
Unresolved Promises
Unresolved promises can also lead to memory leaks if they are not handled properly. When a promise is not resolved or rejected, its associated callbacks and scope remain in memory, preventing garbage collection. Here's an example of how unresolved promises can cause memory leaks:
function fetchDataAsync() { return new Promise((resolve, reject) => { // Some asynchronous operation, but forgot to call resolve() or reject() // This promise will never settle. }); } function processUnresolvedPromises() { for (let i = 0; i < 1000; i++) { // Unresolved promises are created in a loop and not handled. fetchDataAsync().then((data) => { // Do something with the data }); } } processUnresolvedPromises();
In this example, the
fetchDataAsync()
function returns a promise, but it never resolves or rejects. TheprocessUnresolvedPromises()
function creates a large number of promises in a loop, and none of them are handled properly. As a result, all the associated callbacks and scope for each unresolved promise are retained in memory, causing a memory leak.To avoid this, always make sure to handle promises properly, ensuring that they are either resolved or rejected within a reasonable
timeframe
. If you no longer need a promise, make sure to clean up any references to it and its associated callbacks.One way to handle unresolved promises is to use a timeout with
Promise.race()
to handle unresolved promises after a certain period.
To avoid memory leaks, always be mindful of variable scoping, properly remove event listeners, clear intervals/timeouts, and avoid unnecessary data retention. Using modern JavaScript practices and memory management techniques can help mitigate the risk of memory leaks.