What is Memory Leak? How can We Avoid?
Inadequate memory management usually doesn’t have dramatic consequences when dealing with “outdated” web pages. As the user navigates through links and loads new pages, the page information is removed from memory on each load.
The rise of SPA (Single Page Application) encourages us to pay more attention to memory-related JavaScript coding practices. As the application consumes more and more memory, it can severely degrade performance and even cause the browser tab to hang.
In this article, we’ll explore programming patterns that cause memory leaks in JavaScript and explain how to improve memory management.
Read Also : Top Qualifying Interview Questions For JavaScript
What Is a Memory Leak and How to Spot It?
Objects are kept in memory by the browser on the heap, while being accessible from the root directory through the reference string. Garbage Collector is a background process in the engine JavaScript that identifies inaccessible objects and deletes and restores the underlying storage.
A memory leak occurs when an object in memory intended to be cleaned up in a garbage collection cycle is accessed from root through a reference unintended by another remaining object. Keeping redundant objects in memory causes excessive memory usage in the application and can lead to degraded and poor performance.
How do I know that our code is leaking memory? Well, memory leaks are insidious and often difficult to detect and locate. filter is not considered invalid in any way and the browser does not return any errors when running it. If we notice that our page performance is gradually deteriorating, we can use the browser’s built-in tools to determine if it is there is a memory leak and what objects it causes.
Task Manager (not to be confused with the task manager of the operating system). They give us an overview of all the tabs and processes running in the browser. The manager can be accessed in Chrome by pressing Shift+Esc on Linux and Windows, while the one built into Firefox by typing about:performance in the address bar allows us, among other things, to see the JavaScript memory footprint of each tab. our website sits there and does nothing, but the javascript memory usage gradually increases, we most likely have a memory leak.
Developer Tools offers advanced methods of memory management. Recording in the performance tool of Chrome allows us to visually analyze the performance of a page while it is running. Some patterns are typical of memory leaks, such as the increased heap memory usage pattern shown below.
Alternatively, the Chrome and Firefox dev tools provide excellent opportunities to further explore memory usage using the Memory Tool Comparing consecutive heap snapshots shows us where and how much memory was allocated between the two snapshots, along with additional details to help us identify problematic objects in the code.
Common Sources of Memory Leaks in JavaScript Code
Researching the causes of memory leaks is really a search for programming patterns that can cause us to keep references to objects that would otherwise be eligible for Garbage Collection The following is a useful list of places in your code that are more prone to memory leaks and deserve special attention when managing memory.1. Random global variables.
1. Accidental global variables
Global variables are always available in the root directory and are never picked up. Some bugs cause migration variables from local to global scope when not in mode:
- Assign a value to an undeclared variable,
- with ‘this’ pointing to the global object.
function createGlobalVariables() {
leaking1 = 'I leak into the global scope'; // assigning value to the undeclared variable
this.leaking2 = 'I also leak into the global scope'; // 'this' points to the global object
};
createGlobalVariables();
window.leaking1; // 'I leak into the global scope'
window.leaking2; // 'I also leak into the global scope'
Avoid: Strict mode (“use strict”) will help you avoid memory leaks and the console errors in the example above.
2. Closures
Function-bound variables are cleaned up after the function exits the call stack and when there are no more references pointing to it outside the function . Completion keeps referenced variables alive, even if the function’s execution has terminated and its execution context and variable environment are long gone.
function outer() {
const potentiallyHugeArray = [];
return function inner() {
potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
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 PotentialHugeArray
is never returned by any of the functions and cannot be reached, but its size can grow indefinitely depending on how many times we call inner function()
.
How to prevent it: closures are unavoidable and a part of JavaScript, so it’s important:
- understand when the closure was created and what objects it contains,
- Understand the expected lifetime and usage of the closure (especially when ‘it is used as a callback).
3. Timers
Have a setTimeout
or setInterval
The reference to an object in the callback is the most common way to prevent objects from being picked up. When we set the recurring timer in our code ( we can setTimeout
behaves like setInterval
i.e. make it recursive), the reference to the object of the timer callback remains active as long as the callback is callable.
In the following example, data
The object can only be picked up after removing the timer. Since we don't have a reference to setInterval
, can never be deleted and data.hugeString
is kept in memory until the application is stopped, but 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 avoid it: Especially if callback duration is indefinite or indefinite:
- Beware of the objects referenced by the timer callback.
- Use the identifier returned by the timer to cancel it if 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
4. Event listeners
The active event listener prevents all variables collected within its scope from being collected. Once added, the event listener remains in effect until:
- explicitly removed with
removeEventListener()
- the associated DOM element is removed.
Some types of events expect this to last until the user leaves the page, such as buttons intended to be clicked multiple times However, sometimes we want an event listener to run a certain number of times.
const hugeString = new Array(100000).join('x');
document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});
In the example above, an anonymous inline function is used as the event listener, which means it cannot be used with removeEventListener()
. Also the document can't be removed, so we have to stick with the listener and whatever 'it keeps in scope even if we only use it have to fire once.
How to Avoid: We need to stop recording the event — Release the listener whenever it is no longer needed to refer to it and pass it to removeEventListener()
.
function listener() {
doSomething(hugeString);
}
document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here
In case Event listener only needs to be run once , addEventListener()
can take a third parameter, an object that provides additional options. Provided that {once: true}
is passed as third parameter to addEventListener()
, the listener will be automatically deleted once the event is processed once.
document.addEventListener('keyup', function listener() {
doSomething(hugeString);
}, {once: true}); // listener will be removed after running once
5. Cache
If we keep adding memory to the cache, with no unused objects to remove, and no logic limiting the size, the cache can grow indefinitely.
let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const mapCache = new Map();
function cache(obj){
if (!mapCache.has(obj)){
const value = `${obj.name} has an id of ${obj.id}`;
mapCache.set(obj, value);
return [value, 'computed'];
}
return [mapCache.get(obj), 'cached'];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321")
user_1 = null; // removing the inactive user
// Garbage Collector
console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") // first entry is still in cache
user_1. Therefore, we also need to flush the cache of entries that will never be reused.
Possible solution: To solve this problem, we can use Weak Map
. It is a data structure with key references kept weak, accepting only objects as keys. If we use an object as a key and it's the only reference is to that object, the associated entry is removed from the cache and garbage is collected In the following example, after replacing user_1
, the associated entry will be automatically removed from the WeakMap after the next garbage collector.
let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const weakMapCache = new WeakMap();
function cache(obj){
// ...same as above, but with weakMapCache
return [weakMapCache.get(obj), 'cached'];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(weakMapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321"}
user_1 = null; // removing the inactive user
// Garbage Collector
console.log(weakMapCache); // ((…) => "Mark has an id of 54321") - first entry gets garbage collected
Conclusion
When it comes to non-trivial applications, detecting and fixing JavaScript problems and memory leaks can be a very difficult task Understand common causes memory leaks to prevent them from When it comes to memory and performance, the bottom line is user experience and that’s what matters most.