Five common JavaScript memory errors

Author: Jose Granja
Translator: front end Xiaozhi
Source: medium

There are dreams and dry goods. Wechat search [Daqian world] pays attention to this bowl washing wisdom who is still washing dishes in the early morning.
This article GitHub https://github.com/qq449245884/xiaozhi It has been included. There are complete test sites, materials and my series of articles for the interview of front-line large factories.

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 enforce garbage collection, how do we know it works? How much do we know about it?

  • Script execution is paused during this process
  • It frees up memory for inaccessible resources
  • It is uncertain
  • Instead of checking the entire memory at once, it runs in multiple cycles
  • It is unpredictable, but it will be executed when necessary

Does this mean that you don't have to worry about resource and memory allocation? Of course not. If we are not careful, some memory leaks may occur.

What is a memory leak?

A memory leak is an allocated block of memory that the software cannot reclaim.

Javascript provides a garbage collector, but that doesn't mean we can avoid memory leaks. In order to qualify for garbage collection, the object must not be referenced elsewhere. If you hold references to unused resources, this will prevent them from being recycled. This is called unconscious memory retention.

Leaking memory may cause the garbage collector to run more frequently. Because this process will prevent the script from running, it may cause our program to get stuck. With such a card, picky users will certainly notice that if they don't use it well, the product's off-line days will not be over. More serious, it may cause the whole application to collapse, that's gg it.

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

1. Timer monitoring

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

We create a component that calls a callback function to indicate that it is completed after x cycles. I use 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 clicking the Retry button several times, this is the result of using Chrome Dev Tools to obtain memory usage:

When we click the retry button, we can see that more and more memory is allocated. This indicates 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 special 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, when writing code, it is difficult to find this problem. The best way is to abstract components.

React is used here. We can wrap all these 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;

You can do this now when you need to use setInterval:

const handleTimeout = () => ...;

useTimeout(100, handleTimeout);

Now you can use this useTimeout Hook without worrying about memory leakage, which is also the benefit of abstraction.

2. Event monitoring

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

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

function homeShortcuts({ key}) {
    if (key === 'E') {
        console.log('edit widget')
    }
}

// Users log in on the home page and we execute
document.addEventListener('keyup', homeShortcuts); 


// The user does something and then navigates to settings

function settingsShortcuts({ key}) {
    if (key === 'E') {
        console.log('edit setting')
    }
}

// Users log in on the home page and we execute
document.addEventListener('keyup', settingsShortcuts); 

It looks good, 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, you need to be careful when using tools from global objects.

3.Observers

Observers is a Web API function of a browser, which many developers don't know. This is powerful if you want to check for changes in the visibility or size of HTML elements.

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

Although it is very powerful, we should use it carefully. Once you've finished observing the object, remember to cancel it when you're not using it.

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 good. However, what happens to the observer once the component is unloaded? It won't be cleared, so the memory will leak. How can 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 error. In some scenarios, it may be difficult to find it, especially when using the this keyword in the context of Window Execution. Take 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, there will be more heaps.

Another problem may be that a global variable is incorrectly defined:

var a = 'example 1'; // Scope is limited to where var is created
b = 'example 2'; // Add to Window object

To prevent this problem, you can use strict mode:

"use strict"

By using strict patterns, you imply to the JavaScript compiler that you want to protect yourself from these behaviors. You can still use Window when you need it. However, you must use it in a clear way.

How does strict mode affect our previous example:

  • For the addElement function, this is undefined when called from the global scope
  • If const | let | var is not specified on a variable, you will get the following error:
Uncaught ReferenceError: b is not defined

5. Hold DOM reference

DOM nodes also cannot avoid memory leaks. We need to be careful not to save their references. Otherwise, the garbage collector will not be able to clean them up because they are still accessible.

Demonstrate with a short piece of code:

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;

Notice that the addElement function clears the list div 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, it will be removed from the list div, but it is not suitable for garbage collection because it is stored in the elements array.

We monitor the function after several executions:

In the screenshot above, you can see how the node is compromised. How to solve this problem? Clearing the elements array will qualify them for garbage collection.

summary

In this article, we have seen the most common way of memory leakage. Obviously, JavaScript itself does not leak memory. On the contrary, it is caused by unintentional memory retention on the part of developers. As long as the code is clean and tidy and we don't forget to clean it ourselves, there will be no leakage.

It is necessary to understand how memory and garbage collection work in JavaScript. Some developers get the wrong idea that because it is automatic, they don't need to worry about this problem.

~After that, I'm Xiaozhi. I'm a yard farmer who goes home to set up a stall after I retire.

The bugs that may exist after code deployment cannot be known in real time. Afterwards, in order to solve these bugs, we spent a lot of time on log debugging. By the way, we recommend a useful BUG monitoring tool Fundebug.

Original text: https://betterprogramming.pub...

communication

There are dreams and dry goods. Wechat search [Daqian world] pays attention to this bowl washing wisdom who is still washing dishes in the early morning.

This article GitHub https://github.com/qq44924588... It has been included. There are complete test sites, materials and my series of articles for the interview of front-line large factories.

Keywords: Javascript Front-end Vue.js

Added by marksie1988 on Sat, 01 Jan 2022 00:44:09 +0200