Front-end (vue) entry to proficiency course: Entering to learn JavaScript does not provide any memory management operations. Instead, memory is managed by the JavaScript VM through a memory reclamation process called garbage collection .
Since we can't force garbage collection, how do we know it's working? How much do we know about it?
Script execution is paused during this process
It releases memory for inaccessible resources
it is uncertain
It does not check the entire memory at once, but runs in multiple cycles
It is unpredictable but it will perform when necessary
Does this mean there is no need to worry about resource and memory allocation issues? Of course not. If we are not careful, some memory leaks may occur.
A memory leak is a block of allocated memory that software cannot reclaim.
Javascript provides a garbage collector, but that doesn't mean we can avoid memory leaks. To be eligible for garbage collection, the object must not be referenced elsewhere. If you hold references to unused resources, this will prevent those resources from being reclaimed. This is called unconscious memory retention .
Leaking memory may cause the garbage collector to run more frequently. Since this process will prevent the script from running, it may cause our program to freeze. If such a lag occurs, picky users will definitely notice that if they are not happy with it, the product will be offline for a long time. More seriously, it may cause the entire application to crash, which is gg.
How to prevent memory leaks? The main thing is that we should avoid retaining unnecessary resources. Let’s look at some common scenarios.
setInterval()
method repeatedly calls a function or executes a code fragment, with a fixed time delay between each call. It returns an interval ID
ID
uniquely identifies the interval so you can later delete it by calling clearInterval()
.
We create a component that calls a callback function to indicate that it's finished after x
number of loops. I'm using React in this example, but this works with any FE framework.
import React, { useRef } from 'react'; const Timer = ({ cicles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); return; } currentCicles.current++; }, 500); return ( <p>Loading...</p> ); } export default Timer;
At first glance, there seems to be no problem. Don't worry, let's create another component that triggers this timer and analyze its memory performance.
import React, { useState } from 'react'; import styles from '../styles/Home.module.css' import Timer from '../components/Timer'; export default function Home() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); return ( <p className={styles.container}> {showTimer ? ( <Timer cicles={10} onFinish={onFinish} /> ): ( <button onClick={() => setShowTimer(true)}> Retry </button> )} </p> ) }
After a few clicks on the Retry
button, here's the result of using Chrome Dev Tools to get the memory usage:
When we click the retry button, we can see that more and more memory is allocated. This means that the previously allocated memory has not been released. The timer is still running instead of being replaced.
How to solve this problem? The return value of setInterval
is an interval ID, which we can use to cancel this interval. In this particular case, we can call clearInterval
after the component is unloaded.
useEffect(() => { const intervalId = setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); return; } currentCicles.current++; }, 500); return () => clearInterval(intervalId); }, [])
Sometimes, it is difficult to find this problem when writing code. The best way is to abstract the components.
Using React here, we can wrap all this logic in a custom Hook.
import { useEffect } from 'react'; export const useTimeout = (refreshCycle = 100, callback) => { useEffect(() => { if (refreshCycle <= 0) { setTimeout(callback, 0); return; } const intervalId = setInterval(() => { callback(); }, refreshCycle); return () => clearInterval(intervalId); }, [refreshCycle, setInterval, clearInterval]); }; export default useTimeout;
Now whenever you need to use setInterval
, you can do this:
const handleTimeout = () => ...; useTimeout(100, handleTimeout);
Now you can use this useTimeout Hook
without worrying about memory leaks, which is also the benefit of abstraction.
Web API provides a large number of event listeners. Earlier, we discussed setTimeout
. Now let's look at addEventListener
.
In this example, we create a keyboard shortcut function. Since we have different functions on different pages, different shortcut key functions will be created
function homeShortcuts({ key}) { if (key === 'E') { console.log('edit widget') } } // When the user logs in on the homepage, we execute document.addEventListener('keyup', homeShortcuts); // The user does something and then navigates to the settings function settingsShortcuts({ key}) { if (key === 'E') { console.log('edit setting') } } // When the user logs in on the homepage, we execute document.addEventListener('keyup', settingsShortcuts);
It still looks fine, except that the previous keyup
is not cleaned up when executing the second addEventListener
. Rather than replacing our keyup
listener, this code will add another callback
. This means that when a key is pressed, it triggers two functions.
To clear the previous callback, we need to use removeEventListener
:
document.removeEventListener('keyup', homeShortcuts);
Refactor the above code:
function homeShortcuts({ key}) { if (key === 'E') { console.log('edit widget') } } // user lands on home and we execute document.addEventListener('keyup', homeShortcuts); // user does some stuff and navigates to settings function settingsShortcuts({ key}) { if (key === 'E') { console.log('edit setting') } } // user lands on home and we execute document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', settingsShortcuts);
As a rule of thumb, be very careful when using tools from global objects.
Observers are a browser Web API feature that many developers are unaware of. This is powerful if you want to check for changes in visibility or size of HTML elements.
The IntersectionObserver
interface (part of the Intersection Observer API) provides a method for asynchronously observing the intersection status of a target element with its ancestor elements or top-level document viewport
. The ancestor element and viewport
are called root
.
Although it is powerful, we must use it with caution. Once you've finished observing an object, remember to cancel it when not in use.
Take a look at the code:
const ref = ... const visible = (visible) => { console.log(`It is ${visible}`); } useEffect(() => { if (!ref) { return; } observer.current = new IntersectionObserver( (entries) => { if (!entries[0].isIntersecting) { visible(true); } else { visbile(false); } }, { rootMargin: `-${header.height}px` }, ); observer.current.observe(ref); }, [ref]);
The code above looks fine. However, what happens to the observer once the component is unloaded? It is not cleared and the memory is leaked. How do we solve this problem? Just use the disconnect
method:
const ref = ... const visible = (visible) => { console.log(`It is ${visible}`); } useEffect(() => { if (!ref) { return; } observer.current = new IntersectionObserver( (entries) => { if (!entries[0].isIntersecting) { visible(true); } else { visbile(false); } }, { rootMargin: `-${header.height}px` }, ); observer.current.observe(ref); return () => observer.current?.disconnect(); }, [ref]);
Adding objects to a Window is a common mistake. In some scenarios, it may be difficult to find it, especially when using this
keyword in a Window Execution context. Take a look at the following example:
function addElement(element) { if (!this.stack) { this.stack = { elements: [] } } this.stack.elements.push(element); }
It looks harmless, but it depends on which context you call addElement
from. If you call addElement from the Window Context, the heap will grow.
Another problem might be incorrectly defining a global variable:
var a = 'example 1'; // The scope is limited to the place where var is created b = 'example 2'; // Added to the Window object
To prevent this problem you can use strict mode:
"use strict"
By using strict mode, you signal to the JavaScript compiler that you want to protect yourself from these behaviors. You can still use Window when you need to. However, you must use it in an explicit way.
How strict mode affects our previous example:
For the addElement
function, this
is undefined when called from the global scope
If you don't specify const | let | var
on a variable, you will get the following error:
Uncaught ReferenceError: b is not defined
DOM nodes are not immune to memory leaks either. We need to be careful not to save references to them. Otherwise, the garbage collector will not be able to clean them because they will still be accessible.
Demonstrate it with a small piece of code:
const elements = []; const list = document.getElementById('list'); function addElement() { // clean nodes list.innerHTML = ''; const pElement= document.createElement('p'); const element = document.createTextNode(`adding element ${elements.length}`); pElement.appendChild(element); list.appendChild(pElement); elements.push(pElement); } document.getElementById('addElement').onclick = addElement;
Note that the addElement
function clears the list p
and adds a new element to it as a child element. This newly created element is added to the elements
array.
The next time addElement
is executed, the element will be removed from the list p
, but it is not suitable for garbage collection because it is stored in the elements
array.
We monitor the function after executing it a few times:
See how the node was compromised in the screenshot above. So how to solve this problem? Clearing the elements
array will make them eligible for garbage collection.
In this article, we have looked at the most common ways of memory leaks. It's obvious that JavaScript itself doesn't leak memory. Instead, it is caused by unintentional memory retention on the part of the developer. As long as the code is clean and we don't forget to clean up after ourselves, leaks won't happen.
Understanding how memory and garbage collection work in JavaScript is a must. Some developers get the false sense that since it's automatic, they don't need to worry about this issue.