Common JavaScript memory errors and solutions

Common JavaScript memory errors and solutions

Preface:

JavaScript does not provide any memory management operations. Instead, memory is managed by JavaScript VM through a memory reclamation process known as garbage collection.

Since we can’t force garbage collection, how do we know it works? And what do we know about it?

Script execution is paused during this process It frees memory for inaccessible resources It is non-deterministic It does not check the entire memory at once, but runs in multiple cycles It is unpredictable, but it will execute 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, we may have some memory leaks.

What is a memory leak?

A memory leak is a block of allocated memory that the 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 from elsewhere. If references to unused resources are held, this will prevent those resources from being collected. 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 get stuck. If it gets stuck, picky users will definitely notice it, and if they are dissatisfied with it, the product will be offline soon. In more serious cases, the entire application may crash, and then it will be gg.

How to prevent memory leaks? The main thing is that we should avoid retaining unnecessary resources. Let’s look at some common scenarios.

1. Timer monitoring

setInterval() method calls a function or executes a code snippet repeatedly with a fixed time delay between each call. It returns an interval ID which uniquely identifies the interval so you can remove it later by calling clearInterval() .

We create a component that calls a callback function to signal that it's done after x number of loops. I'm using React in this example, but this applies to 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 ( 
        <div>Loading ...</div> 
    ); 
} 
 
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 ( 
      <div className={styles.container}> 
          {showTimer ? ( 
              <Timer cicles={10} onFinish={onFinish} /> 
          ): ( 
              <button onClick={() => setShowTimer(true)}> 
                Retry 
              </button> 
          )} 
      </div> 
    ) 
} 


After a few clicks on the Retry button, this is the result of getting the memory usage using Chrome Dev Tools :

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 still runs instead of being replaced.

How to solve this problem? The return value of setInterval is an interval ID, which we can use to cancel the interval. In this special case, we can call clearInterval after the component unmounts.

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 component.

Using React here, we can wrap all of 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.

2. Event monitoring

Web API provides a large number of event listeners. Earlier, we discussed setTimeout . Now let's take a look at addEventListener .

In this example, we create a keyboard shortcut function. Since we have different functions on different pages, we will create different shortcut key functions

function homeShortcuts({ key}) { 
    if (key === 'E') { 
        console.log('edit widget') 
    } 
} 
 
// The user logs in on the home page, we execute document.addEventListener('keyup', homeShortcuts);  
 
 
// User does something and then navigates to settings function settingsShortcuts({ key}) { 
    if (key === 'E') { 
        console.log('edit setting') 
    } 
} 
 
// The user logs in on the home page, we execute document.addEventListener('keyup', settingsShortcuts);  


It still looks fine, except that the previous keyup is not cleaned up when the second addEventListener is executed. Instead of replacing our keyup listener, this code will add another callback . This means that when a key is pressed, it will trigger 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 the global object.

3.Observers

Observers is a browser Web API feature that many developers are not aware of. This is powerful if you want to check for changes in visibility or size of HTML elements.

The IntersectionObserver interface (subordinate to the Intersection Observer API) provides a way to asynchronously observe the intersection state of a target element with its ancestor elements or the top-level document window (viewport). The ancestor element and the viewport are called roots.

Although it is powerful, we must use it with caution. Once you are done 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 above code looks good. However, once the component is unmounted, what happens to the observer? It doesn’t get cleared, and you have a memory leak.

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]); 


4. Window Object

Adding objects to Window is a common mistake. In some scenarios, it might be hard to find it, especially when using the this keyword in 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 Window Context , the pile will grow.

Another problem could be incorrectly defining a global variable:

var a = 'example 1'; // scoped to 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 hint to the JavaScript compiler that you want to protect yourself from these behaviors. You can still use Window when you need it. However, you have to 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’ll get the following error:
Uncaught ReferenceError: b is not defined 


5. Hold DOM reference

DOM nodes are not immune to memory leaks either. We need to be careful not to store references to them. Otherwise, the garbage collector will not be able to clean them up because they are still reachable.

Let's demonstrate with a small code snippet:

const elements = []; 
const list = document.getElementById('list'); 
 
function addElement() { 
    // clean nodes 
    list.innerHTML = ''; 
 
    const divElement = document.createElement('div'); 
    const element = document.createTextNode(`adding element ${elements.length}`); 
    divElement.appendChild(element); 
 
 
    list.appendChild(divElement); 
    elements.push(divElement); 
} 
 
document.getElementById('addElement').onclick = addElement; 


Note: The addElement function clears the list div and adds a new element as a child to it. The newly created element is added to the elements array.

The next time addElement is executed, the element will be removed from the list div, but it will not be eligible for garbage collection because it is stored in the elements array.

We monitor the function after executing it several times:


See how the node is leaked in the screenshot above. So how do you fix this? Clearing elements array will make them eligible for garbage collection.

Summarize:

In this article, we have seen the most common types of memory leaks. Obviously, JavaScript itself does not leak memory. Instead, it is caused by unintentional memory retention on the developer's part. 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 wrong idea and think that since it's automatic, they don't need to worry about it.

This is the end of this article about common JavaScript memory errors. For more information about JavaScript memory errors, please search 123WORDPRESS.COM’s previous articles or continue to browse the following related articles. I hope you will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • Common JavaScript memory errors and solutions
  • JavaScript Misunderstanding: Don't Care About Memory Management

<<:  How to set the position of the block element in the middle of the window

>>:  Example code for implementing beautiful clock animation effects with CSS

Recommend

960 Grid System Basic Principles and Usage

Of course, there are many people who hold the oppo...

How to use jconsole to monitor remote Tomcat services

What is JConsole JConsole was introduced in Java ...

Getting Started Guide to Converting Vue to React

Table of contents design Component Communication ...

In-depth understanding of Vue's method of generating QR codes using vue-qr

Table of contents npm download step (1) Import (2...

The process of installing and configuring nginx in win10

1. Introduction Nginx is a free, open source, hig...

503 service unavailable error solution explanation

1. When you open the web page, 503 service unavai...

SVG+CSS3 to achieve a dynamic wave effect

A vector wave <svg viewBox="0 0 560 20&qu...

Detailed explanation of the use of Vue3 state management

Table of contents background Provide / Inject Ext...

IDEA configuration process of Docker

IDEA is the most commonly used development tool f...

Teach you how to use vscode to build a react-native development environment

question The code has no prompt: Many non-front-e...

Two methods to implement MySQL group counting and range aggregation

The first one: normal operation SELECT SUM(ddd) A...

jQuery implements accordion effects

This article shares the specific code of jQuery t...

Solution for Vue routing this.route.push jump page not refreshing

Vue routing this.route.push jump page does not re...

How to bypass unknown field names in MySQL

Preface This article introduces the fifth questio...