I don’t know if any of you have ever been asked the question: If the page is stuck, what do you think might be the cause? Is there any way to pinpoint the cause and fix it?

This is a very broad and deep question, and it involves a lot of page performance optimization questions. I vaguely remember my answer when I was asked this question in an interview:

  1. First, it will check whether there are too many network requests, resulting in slow data return. You can do some caching appropriately
  2. It is also possible that the bundle of a resource is too large, so consider splitting it up
  3. Then check the JS code to see if there are too many loops somewhere that occupy the main thread for too long
  4. The browser rendered too many things in one frame, resulting in a lag
  5. There can be a lot of repeated rearrangements and redraws during page rendering
  6. emmmmmm…. I don’t know the

It was later learned that the sensory lag of the long-running page could also be caused by a memory leak

🌟 Definition of memory leak

So what is a memory leak? A memory leak, as defined by other big names, is a failure to free memory that is no longer used due to negligence or some program error. In simple terms, if a variable occupies 100 MB of memory and you do not need it, but the variable has not been reclaimed manually or automatically, it still occupies 100 MB of memory. This is a waste of memory, which is called a memory leak

🌼 JS data store

The memory space of JavaScript is divided into stack memory and heap memory. The former is used to hold some simple variables, and the latter is used to hold complex objects

  • Simple variables refer to basic JS data types, such as:String,Number,Boolean,null,undefined,Symbol,BigInt
  • Complex objects refer to JS reference data types, such as:Object,Array,Function.

🌴 JS garbage collection mechanism

According to the definition of a memory leak, a variable or data that is no longer used or needed is a garbage variable or data, and if it is kept in memory, it may end up occupying too much memory. Then the garbage data needs to be collected, which introduces the concept of garbage collection mechanism

There are manual and automatic garbage collection mechanisms

For example, C/C++ uses the mechanism of manual reclamation, that is, first use the code to allocate a certain amount of memory for a variable, and then use the code to manually release the memory when it is no longer needed

JavaScript, on the other hand, uses an automatic collection mechanism, which means we don’t need to care when to allocate how much memory for variables, or when to free memory, because it’s all automatic. But that doesn’t mean we don’t need to care about memory management !!!! Otherwise, there would be no memory leaks discussed in this article

Let’s talk about JavaScript garbage collection

In general, variables in the global state (window) are not automatically reclaimed, so let’s discuss the local scope of memory reclamation

function fn1 () {
    let a = {
        name: 'and'
    }

    let b = 3

    function fn2() {
        let c = [1.2.3]
    }

    fn2()

    return a
}

let res = fn1()
Copy the code

The call stack of the above code looks like this:

On the left is the stack space for some execution context and basic type data; The heap space on the right is used to store some complex object data

When the code is executed to fn2(), the execution context in the stack space from top to bottom is fn2 function execution context => fn1 function execution context => global execution context

After the internal execution of the fn2 function is complete, it is time to exit the fn2 function execution context, that is, the arrow moves down, and the fn2 function execution context is cleared and the stack memory space is freed, as shown in the figure:

After the internal execution of fn1 function is completed, it is time to exit the execution context of Fn1 function, that is, when the arrow moves down again, the execution context of Fn1 function will be cleared and the corresponding stack memory space will be released, as shown in the figure:

This is in the global execution context. The JavaScript garbage collector will walk through the call stack every once in a while, assuming that the garbage collection mechanism is triggered. When walking through the call stack, it finds that variables B and C are not referenced by any variables, so it considers them garbage data and marks them. Since the fn1 function returns the variable a after execution and stores it in the global variable res, it is considered to be active data and marked accordingly. When idle, all variables marked with garbage data will be cleared to release the corresponding memory, as shown in the figure:

From this we draw several conclusions:

  1. JavaScriptThe garbage collection mechanism is automated, and the garbage data is identified and cleaned by tagging
  2. After leaving the local scope, variables in that scope are cleared if they are not referenced by the outer scope

Additional: JavaScript garbage collection mechanism has many steps, the above mentioned only talked about mark-clean, in fact, there are other processes, this brief introduction will not expand the discussion. For example, mark-collation. After some garbage data is cleared, a large area of discontinuous memory may be left behind. As a result, continuous memory may not be allocated for some objects in the future. Because JavaScript is running on the main thread, the execution of garbage collection mechanism will suspend the running of JS. If the execution of garbage collection takes a long time, it will bring obvious lag phenomenon to users. Therefore, garbage collection mechanism will be divided into small tasks interspersed among JS tasks, namely, alternate execution. Try to make sure there’s no obvious lag

👋 Chrome devTools to view memory

Before looking at some common memory leak scenarios, let’s briefly explain how to use Chrome’s developer tools to view JS memory

The first step is to turn on Chrome in non-trace mode. This is to block the impact of the Chrome plugin on the memory footprint of our later tests

Then open the developer tools and go to the Performance column. You can see that there are some functional buttons inside it, such as: Start recording button; Refresh page button; Clear record button; Record and visualize JS memory, node, event listener button; Trigger the garbage collection button and so on

Briefly record the Baidu page and see what we can get, as shown in the GIF below:

In the diagram above, we can see the lowest and highest values of the JS Heap, Documents, Nodes, DOM Nodes, and GPU memory, and the process of loading the page from zero to complete. That’s our main focus

Take a look at the Memory column in the developer tool, which is mainly used to record the page heap Memory and the dynamic allocation of JS heap Memory with the loading time line

Heap snapshots are like cameras that record the heap memory of your current page. Each snapshot creates a snapshot, as shown in the figure below:

As shown in the figure above, we first took a snapshot and recorded that the heap memory was 13.9MB, then clicked some buttons on the page and took another snapshot and recorded that the heap memory was 13.4MB. And click the corresponding snapshot record, you can see all the variables in the memory (structure, percentage of the total occupied memory…)

Then we can take a look at the page dynamic memory changes, as shown in the figure below:

After we start recording, we can see the fluctuating blue and gray bar chart in the upper right corner of the graph, where blue indicates the memory occupied under the current time line; Gray indicates that the previously occupied memory space has been cleared and released.

From the process above, we can see that the page corresponding to the initial TAB occupies a certain amount of heap memory space and becomes a blue bar. After clicking another TAB, the content corresponding to the original TAB disappears and the original blue bar turns to gray (indicating that the original memory space has been freed). At the same time, the new TAB page also takes up a certain amount of heap memory space. So we can look at this graph to see how much memory is being used and cleared

🔥 Memory leak scenario

So what exactly are the situations where memory leaks can occur? Here are some common ones:

  1. Improper use of closures causes memory leaks
  2. The global variable
  3. Detached DOM nodes
  4. Console printing
  5. Forgetting timer

Let’s take a look at the various situations and try to capture the problem using the two methods I’ve just described

1. Improper use of closures

Article at the beginning of case, after the exit fn1 function execution context, in this context variable should be regarded as a junk data to the recovery, but in the end because of fn1 function will return to and assigned to the variable a global variable res, the produced reference for variable a, so a marked as activity variables and has been holding the corresponding memory, Assuming that the variable res is not used later, this is an example of closure misuse

Next, try using Performance and Memory to look at the Memory leaks caused by closures. To make the results of the Memory leaks more obvious, let’s change the example at the beginning of this article to look like this:

<button onclick="myClick()">Execute the fn1 function</button>
<script>
    function fn1 () {
        let a = new Array(10000)  // Here we set a large array object

        let b = 3

        function fn2() {
            let c = [1.2.3]
        }

        fn2()

        return a
    }

    let res = []  

    function myClick() {
        res.push(fn1())
    }
</script>
Copy the code

A button is set to add the return value of fn1 to the global array variable res each time it is executed. This is to be seen in the graph of PerformAcne, as shown below:

We manually trigger a garbage collection at the start of each recording to confirm an initial heap memory baseline for later comparison, then we click the button a few times to add a few large array objects to the global array variable RES, and finally trigger a garbage collection again. It was found that the JS Heap curve of the recording results started to step up, and the height of the final curve was higher than the base line, indicating that there might be a memory leak

When we know that a Memory leak exists, we can use Memory to more explicitly identify and locate the problem

First, you can use the Allocation instrumentation on timeline to confirm the problem, as shown in the figure below:

After we click the button, a blue bar appears on the dynamic memory allocation graph, and after we trigger garbage collection, the blue bar does not turn into a gray bar, indicating that the previously allocated memory has not been cleared

So at this point we can more clearly confirm that the memory leak problem exists, and then we can accurately locate the problem, using Heap Snapshot to locate the problem, as shown in the figure:

Click the snapshot to record the initial memory for the first time, and then click the snapshot again after clicking the button for many times to record the memory at this time. We found that the memory space changed from 1.1m to 1.4m. Then we selected the second snapshot record, and you can see the field of All Objects in the upper right corner. It shows that the currently selected snapshot records the allocation of all objects. However, what we want to know is the difference between the second snapshot and the first snapshot, so select Object Allocated between Snapshot1 and Snapshot2. That is, the memory object allocation of the first snapshot is different from that of the second snapshot. In this case, you can see that the percentage of the Array is high. It can be initially judged that there is a problem with the variable

This is a simple way to identify and locate memory leaks caused by closures

2. Global variables

Global variables are generally not garbage collected, as mentioned at the beginning of this article. Of course, this does not mean that all variables cannot exist globally, but sometimes some variables will be lost to the global because of carelessness. For example, if a variable is assigned to a variable without declaring it, it will be created globally, as shown in the following:

function fn1() {
    // The variable name is not declared here
    name = new Array(99999999)
}

fn1()
Copy the code

In this case, a variable name is automatically created globally, and a large array is assigned to name, and because it is a global variable, the memory space is never freed

Solution, I usually want to pay more attention to, do not in the variable is not declared before the value, or can also open strict mode, so that you will unwittingly make mistakes, receive error warning, such as:

function fn1() {
    'use strict';
    name = new Array(99999999)
}

fn1()
Copy the code

3. Separated DOM nodes

What is a DOM node? Let’s say you manually remove a DOM node that should have freed the memory of the dom node, but the code still has a reference to the removed node, and the memory of the dom node cannot be freed. For example:

<div id="root">
    <div class="child">I'm a child element</div>
    <button>remove</button>
</div>
<script>

    let btn = document.querySelector('button')
    let child = document.querySelector('.child')
    let root = document.querySelector('#root')
    
    btn.addEventListener('click'.function() {
        root.removeChild(child)
    })

</script>
Copy the code

The global variable child has a reference to the node, so the node cannot be released. Try using the Memory snapshot function to detect this, as shown in the figure below:

In the detached box, go to the second snapshot and say “detached”. In the detached box, go to the second snapshot and say “detached”. In the detached box, go to “Detached”. All detached node objects that have not been cleared are then displayed

The solution is shown in the figure below:

<div id="root">
    <div class="child">I'm a child element</div>
    <button>remove</button>
</div>
<script>
    let btn = document.querySelector('button')

    btn.addEventListener('click'.function() {  
        let child = document.querySelector('.child')
        let root = document.querySelector('#root')

        root.removeChild(child)
    })

</script>
Copy the code

The change is as simple as moving the reference to the. Child node into the click callback function. When you remove the node and exit the callback function, the reference to the. Child node is automatically cleared.

The obvious result is that there are no memory leaks after this processing

4. Console printing

Does console printing also cause memory leaks ???? Yes, if the browser doesn’t always hold information about the objects we print, why do we see the specific data every time we open the control Console? Let’s start with some test code:

<button>button</button>
<script>
    document.querySelector('button').addEventListener('click'.function() {
        let obj = new Array(1000000)

        console.log(obj);
    })
</script>
Copy the code

We created a large array object in the button click callback event and printed it, using performance to verify:

To start recording, first trigger a garbage collection to clear the initial memory, then click the button three times, that is, three click events, and finally trigger a garbage collection. Viewing the recording results, it is found that the JS Heap curve rises in a ladder, and the final height remains much higher than the initial base line, indicating that the large array object obj created by each click event is saved by the browser because of console.log and cannot be recycled

Then comment out console.log and take a look at the result:

<button>button</button>
<script>
    document.querySelector('button').addEventListener('click'.function() {
        let obj = new Array(1000000)

        // console.log(obj);
    })
</script>
Copy the code

Performance is shown in the figure:

As you can see, each obJ created without printing is destroyed immediately, and the final garbage collection trigger is as high as the initial baseline, indicating that there are no memory leaks

Similarly, console.log can be further validated using Memory

  • No commentconsole.log

  • Commented outconsole.log

A quick summary: In a development environment, you can use console printing to facilitate debugging, but in a production environment, try not to print data on the console as much as possible. So we often see something like this in our code:

// If in the development environment, print the variable obj
if(isDev) {
    console.log(obj)
}
Copy the code

This prevents unnecessary variables from taking up memory space in the production environment. Similarly, except for console.log, console.error, console.info, console.dir, etc., should not be used in the production environment

The forgetting timer

In fact, the timer is usually a lot of people will ignore a problem, such as the definition of the timer will not be considered to clear the timer, which will actually cause a certain memory leak. Here’s a code example:

<button>Start timer</button>
<script>

    function fn1() {
        let largeObj = new Array(100000)

        setInterval(() = > {
            let myObj = largeObj
        }, 1000)}document.querySelector('button').addEventListener('click'.function() {
        fn1()
    })
</script>
Copy the code

Fn1 creates a large array largeObj and a setInterval timer. The callback function of the timer simply refers to the variable largeObj. Let’s look at the total memory allocation.

After clicking the button to execute fn1 function, it exits the execution context of the function, and then the local variables in the function body should be cleared. However, the recording result of performance in the figure shows that there is a Memory leak, that is, the final curve height is higher than the base line height, so use Memory to confirm again:

After we click the button, we can see a blue bar in the dynamic memory allocation diagram, indicating that the browser allocated a memory for the variable largeObj, but the memory was not released afterwards, indicating that there is a memory leak problem. The reason for this is that the setInterval callback has a reference to largeObj, and the timer has not been cleared, so largeObj will not be freed

To solve this problem, let’s say we only need to let the timer execute three times. Let’s change the code:

<button>Start timer</button>
<script>
    function fn1() {
        let largeObj = new Array(100000)
        let index = 0

        let timer = setInterval(() = > {
            if(index === 3) clearInterval(timer);
            let myObj = largeObj
            index ++
        }, 1000)}document.querySelector('button').addEventListener('click'.function() {
        fn1()
    })
</script>
Copy the code

Now let’s look at performance and memory to see that there are no memory leaks yet

  • performance

The result of this recording shows that the height of the final curve is the same as the height of the initial reference line, indicating that there is no memory leak

  • memory

As an explanation, the first blue bar in the picture is because I refreshed the page after recording, so you can ignore it; Then we click the button and see a blue bar again. At this time, the memory is allocated for the variable largeObj in fn1 function. After 3s, the memory is freed again, which becomes a gray bar. So we can conclude that there is no memory leak in this code

Simple summary: everyone in the usual use of the timer, if not in use after the timer must be cleared, otherwise there will be the case in this case. In addition to setTimeout and setInterval, there’s another API that browsers provide that might have this problem: requestAnimationFrame

👍 summary

In the process of the project, if you encounter some performance problems that may be related to memory leaks, you can refer to the five cases listed in this article to investigate, and you will be sure to find the problem and give solutions.

While JavaScript garbage collection is automatic, it is sometimes necessary to consider manually clearing the memory of certain variables. For example, if you know that a variable is no longer needed, it will still be referenced by an external variable and the memory will not be freed. You can re-assign the variable with NULL to free its memory during subsequent garbage collection phases.

😊 I am also due to a memory leak in the business caused by the page card, so I think of writing such an article. Of course, this is not to say that the page freezes are caused by memory leaks, there may be other reasons

If you have any suggestions or questions about how to troubleshoot memory leaks, you can discuss them in the comments section ~ ✌️

Original is not easy, remembergive a like👍 support for oh ~ 😘