This article takes you to understand how to troubleshoot page jams caused by memory leaks

This article takes you to understand how to troubleshoot page jams caused by memory leaks

I don t know if everyone here has been asked such a question: If the page freezes, what do you think may be the cause? Is there any way to lock the cause and solve it?

This is a very broad and in-depth question. It involves a lot of page performance optimization issues. I vaguely remember the answer when I was asked this question in the interview:

  1. 1. it will check if there are too many network requests, which results in slow data return. You can do some caching appropriately
  2. It is also possible that the bundle of a certain resource is too large, you can consider splitting it
  3. Then check the js code to see if there are too many loops somewhere causing the main thread to be occupied for too long
  4. The browser has rendered too many things in a certain frame, resulting in a freeze
  5. During the page rendering process, there may be many repeated rearrangements and redraws
  6. emmmmmm.... I don t know anymore

?? Later I learned that sensory long-running page freezes may also be caused by memory leaks

The definition of memory leak

So what is a memory leak? With the help of the definition given by other big guys, memory leak refers to the situation where memory that is no longer used is not released due to negligence or some program error. To put it simply? Suppose a variable occupies 100M of memory, and you can't use this variable, but this variable is not recycled manually or automatically, that is, it still occupies 100M of memory space, which is a waste of memory , That is, a memory leak

JS data storage

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

  • Simple variables refer to the basic data types of JS, for example:
    String
    ,
    Number
    ,
    Boolean
    ,
    null
    ,
    undefined
    ,
    Symbol
    ,
    BigInt
  • The complex object refers to the reference data type of JS, for example:
    Object
    ,
    Array
    ,
    Function
    ...

JS garbage collection mechanism

According to the definition of memory leak, some variables or data are no longer used or needed, then they are garbage variables or garbage data. If they are kept in memory, it may eventually lead to excessive memory usage. At this time, these garbage data need to be recycled, and the concept of garbage collection mechanism is introduced here .

The garbage collection mechanism is divided into two types : manual and automatic

E.g

C/C++
The manual recycling mechanism is adopted, that is, the code is used to allocate a certain amount of memory to a variable, and then the code is used to manually release the memory when it is no longer needed

and

JavaScript
The mechanism adopted is automatic recovery, that is, we don't need to care about when to allocate and how much memory for the variable, nor do we need to care about when to release the memory, because all of this is automatic. But this does not mean that we do not need to care about memory management! ! ! ! Otherwise, there will be no memory leaks discussed in this article.

Let's talk about it next

JavaScript
Garbage collection mechanism

Usually variables in the global state (window) will not be automatically recovered, so let's discuss? Memory recovery under the local scope

function fn1 () { let a = { name : 'zero one' } let b = 3 function fn2 () { let c = [ 1 , 2 , 3 ] } fn2() return a } the let RES = Fn1 () to copy the code

The call stack of the above code is shown in the figure below:

The left side of the figure is the stack space , which is used to store some execution context and basic type data; the right side is the heap space , which is used to store some complex object data

When the code is executed

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

To be

fn2
After the internal execution of the function is completed, it is time to exit
fn2 function execution context
, That is, the arrow moves down, at this time
fn2 function execution context
Will be cleared and released the stack memory space, as shown in the figure:

To be

fn1
After the internal execution of the function is completed, it is time to exit
fn1 function execution context
, That is, the arrow moves down again, at this time
fn1 function execution context
Will be cleared and release the corresponding stack memory space, as shown in the figure:

At this time in the global execution context. ?

JavaScript
The garbage collector will traverse the call stack every once in a while, assuming that the garbage collection mechanism is triggered at this time, when the variable is found when traversing the call stack
b
And variables
c
They are not referenced by any variables, so they are considered junk data and marked. because
fn1
After the function is executed, the variable
a
Returned out and stored in a global variable
res
Therefore, it is deemed as activity data and marked accordingly. When it is free, all variables marked with junk data will be cleared, and the corresponding memory will be released, as shown in the figure:

?? From this we draw a few conclusions:

  1. JavaScript
    The garbage collection mechanism of is automatically executed, and the garbage data will be identified and removed through the mark
  2. After leaving the local scope, if the variable in the scope is not referenced by the external scope, it will be cleared later

supplement:

JavaScript
There are many steps in the garbage collection mechanism, the above only talked about
Mark-clear
In fact, there are other processes, so I won t discuss them briefly here. E.g:
Mark-up
, After clearing some garbage data and releasing a certain amount of memory space, a large area of discontinuous memory fragments may be left, which may not be able to allocate continuous memory for some objects in the future. At this time, you need to organize the memory space;
Alternate execution
,because
JavaScript
It runs on the main thread, so it will pause when the garbage collection mechanism is executed
js
If the execution time of garbage collection is too long, it will bring obvious lag to users, so the garbage collection mechanism will be divided into small tasks, interspersed in
js
During the task, it is executed alternately, as far as possible to ensure that it will not bring an obvious sense of lag

Chrome devTools to view memory

Before understanding some common memory leak scenarios, let s briefly introduce how to use

Chrome
Developer tools to view
js
Memory situation

First open

Chrome
Incognito mode, the purpose of this is to block
Chrome
The impact of the plug-in on the memory usage of our subsequent test

Then open

Developer tools
,turn up
Performance
In this column, you can see that there are some function buttons inside, such as: start recording button; refresh page button; clear record button; record and visualize js memory, nodes, event listener buttons; trigger garbage collection mechanism buttons, etc.

Simply record the Baidu page to see what we can get, as shown in the following animation:

From the above picture, we can see that in the process from zero to the completion of the page loading?

JS Heap (js heap memory)
,
documents
,
Nodes (DOM nodes)
,
Listeners
,
GPU memory (? GPU memory)
The minimum value, maximum value and the trend curve over time, which are also our main points of concern

Let s take a look at the developer tools

Memory
A column, which is mainly used to record the specific situation of the page heap memory and
js
Dynamic allocation of heap memory with load timeline

The heap snapshot is like a camera, it can record the heap memory of your current page, and a snapshot record will be generated for each snapshot, as shown in the figure:

As shown in the figure above, a snapshot was taken at the beginning and recorded that the heap memory space occupied by

13.9MB
, And then we clicked some buttons on the page, and performed another snapshot, recording the heap memory space occupied at that time as
13.4MB
. And click on the corresponding snapshot record, you can see the variables in all memory at that time (structure, percentage of total memory occupied...)

Then we can also look at the dynamic memory changes of the page, as shown in the figure:

After starting the recording, we can see the undulating blue and gray bar graphs in the upper right corner of the figure, where blue indicates the memory occupied under the current timeline; gray indicates that the previously occupied memory space has been cleared and released.

From the process of the above figure, we can see that we are at the beginning

tab
The corresponding displayed page occupies a certain amount of heap memory space and forms a blue column. When you click on other
tab
After the original
tab
The corresponding content disappears, and the original blue column becomes gray (indicating that the memory space occupied by the original has been released), and the new
tab
The corresponding displayed page also occupies a certain amount of heap memory space. Therefore, we can check the memory occupation and clearing situation according to this picture in the future.

Scenario of memory leak

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

  1. Improper use of closures causes memory leaks
  2. Global variable
  3. Separate DOM node
  4. Printing from the console
  5. Forgotten timer

Next, I will introduce various situations and try to catch the problem with the two methods just mentioned.

1. Improper use of closures

In the example at the beginning of the article, after exiting

fn1 function execution context
After the variables in that context
a
It should be recycled as garbage data, but because
fn1
The function will eventually change the variable
a
Return and assign to global variable
res
, Which produces a pair of variables
a
Reference, so the variable
a
Is marked as an active variable and has been occupying the corresponding memory, assuming that the variable
res
If you don t use it later, this is an example of improper use of closures.

Next try to use

Performance
with
Memory
Let's take a look at the memory leak caused by closures. In order to make the result of the memory leak more obvious, let's slightly modify the example at the beginning of the article. The code is as follows:

< button onclick = "myClick()" > Execute fn1 function </button > < script > function fn1 () { let a = new Array ( 10000 ) //Here is a very 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 code

A button is set, and each time it is executed, it will

fn1
The return value of the function is added to the global array variable
res
In order to be able to
performacne
The effect can be seen in the graph, as shown in the figure:

The garbage collection mechanism is manually triggered at the beginning of each recording. This is to confirm an initial heap memory baseline for later comparison. Then we clicked the button several times to go to the global array variable

res
Added several relatively large array objects, and finally triggered a garbage collection again, and found that the recording result
JS Heap
The curve has just started to rise in steps, and the height of the final curve is higher than the baseline, indicating that there may be a memory leak.

When we know that there is a memory leak, we can use

Memory
To more clearly identify the problem and locate the problem

First you can use

Allocation instrumentation on timeline
To confirm the problem, as shown in the following figure:

Every time 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, that is, the previously allocated memory is not Cleared

So at this time, we can more clearly confirm that the memory leak problem exists, and then we can accurately locate the problem, and we can use

Heap snapshot
To locate the problem, as shown in the figure:

The first time we clicked the snapshot to record the initial memory situation, and then we clicked the button several times and then clicked the snapshot again to record the memory situation at this time, and found that the original

1.1M
The memory space becomes
1.4M
Memory space, and then we select the second snapshot record, you can see that there is a
All objects
The field indicates that it shows the allocation of all objects in the currently selected snapshot record, and what we want to know is the difference between the second snapshot and the first snapshot, so choose
Object allocated between Snapshot1 and Snapshot2
, That is, display the memory object allocation of the difference between the first snapshot and the second snapshot. At this time, you can see
Array
The percentage of is very high. It can be initially judged that there is a problem with this variable. After you click to view details, you can view the specific data corresponding to the variable.

The above is a method to judge the memory leak caused by the closure and simply locate it.

2. Global variables

Global variables are generally not garbage collected, as mentioned at the beginning of the article. Of course, this is not to say that variables cannot be global, but sometimes some variables are lost to the global due to negligence. For example, if a variable is not declared, but a variable is directly assigned, it will cause the variable to be created globally, as shown below Show:

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

In this case, a variable is automatically created globally

name
, And assign a large array to
name
, And because it is a global variable, the memory space will never be released

As for the solution, you should pay more attention to it. Don't assign a value before the variable is declared, or you can turn on the strict mode, so that you will receive an error warning when you make a mistake without knowing it, for example:

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

3. Separated DOM node

What is DOM node ? Suppose you manually removed a

dom
Node that should have released the
dom
The memory occupied by the node, but some code still has a reference to the removed node due to negligence, and eventually the memory occupied by the node cannot be released. For example, this situation:

< div id = "root" > < div class = "child" > I am 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 code

What this code does is to remove it after clicking the button

.child
Node, although after clicking, the node is indeed from
dom
Was removed, but the global variable
child
There is still a reference to the node, so the memory of the node cannot be released, you can try
Memory
To check the snapshot function, as shown in the figure:

Similarly, first record a snapshot of the initial state, and then click the remove button, and then click the snapshot again. At this time, we can't see any change in the memory size, because the memory occupied by the removed node is too small to be ignored, but We can click on the second snapshot record and enter in the filter box

detached
, So it will show all the node objects that are detached but not cleared

The solution is shown in the figure below:

< div id = "root" > < div class = "child" > I am 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 code

The change is simple, it will be

.child
The reference of the node is moved to
click
In the callback function of the event, when the node is removed and the execution of the callback function is exited, the reference to the node will be automatically cleared, so naturally there will be no memory leak. Let's verify it, as shown in the figure below. Show:

The result is obvious, so there is no memory leak after processing

4. The printing of the console

Will printing from the console also cause memory leaks? ? ? ? Yes, if the browser does not always save the information of our print object, why can we control it every time we open it

Console
When you see the specific data? First look at a piece of test code:

< button > button </button > < script > document .querySelector( 'button' ) .addEventListener ( 'click' , function () { let obj = new Array ( 1000000 ) console .log(obj); }) </script > copy code

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

performance
Let s 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 are executed, and finally a garbage collection is triggered. Check the recording results and find

JS Heap
The curve rises in steps, and the final height maintained is much higher than the initial baseline, which shows that a large array object is created every time the click event is executed.
obj
All because of
console.log
Saved by the browser and cannot be recycled

Comment out next

console.log
, Look at the results again:

< button > button </button > < script > document .querySelector( 'button' ) .addEventListener ( 'click' , function () { let obj = new Array ( 1000000 ) //console.log(obj); }) </script > copy code

performance
as the picture shows:

You can see that there is no printing, every time you create

obj
All were destroyed immediately, and the garbage collection mechanism was finally triggered and the initial baseline was as high as the initial baseline, indicating that there is no memory leak.

In fact, the same is true,

console.log
Can also be used
Memory
To further verify

  • Uncommented
    console.log

  • Commented out
    console.log

Finally, a brief summary: In a development environment, you can use the console to print to facilitate debugging, but in a production environment, you should not print data on the console as much as possible. So we often see operations similar to the following in the code:

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

This prevents useless variable printing in the production environment from occupying a certain amount of memory space, except for

console.log
Outside,
console.error
,
console.info
,
console.dir
Don't use it in a production environment

5. The Forgotten Timer

In fact, the timer is also a problem that many people usually ignore. For example, after the timer is defined, the timer is no longer considered to be cleared. This will actually cause a certain memory leak. Look at a code example:

< button > Turn on the timer </button > < script > function fn1 () { let largeObj = new Array ( 100000 ) setInterval ( () => { let myObj = largeObj }, 1000 ) } document .querySelector( 'button' ) .addEventListener( ' click' , function () { fn1() }) </script > copy code

This code is executed after the button is clicked

fn1
function,
fn1
A large array object is created in the function
largeObj
, And created a
setInterval
Timer, the callback function of the timer simply refers to the variable
largeObj
, Let s take a look at its overall memory allocation:

Logically speaking, click the button to execute

fn1
After the function, it will exit the execution context of the function, and the local variables in the function body should be cleared immediately, but in the figure
performance
The recording results show that there seems to be a memory leak problem, that is, the final curve height is higher than the baseline height, then use
Memory
To confirm once:

After we click the button, a blue bar appears on the dynamic memory allocation graph, indicating that the browser is a variable

largeObj
A section of memory was allocated, but this section of memory was not released afterwards, indicating that there is indeed a memory leak problem. The reason is actually because
setInterval
Variable within the callback function
largeObj
There is a reference relationship, and the timer has not been cleared, so the variable
largeObj
The memory will naturally not be released

So how do we solve this problem? Assuming that we only need to execute the timer three times, then we can change the code:

< button > Turn on the 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 code

Now we pass

performance
with
memory
Let's see if there is still no memory leak problem

  • performance

It can be seen from the results of this recording that the height of the final curve is the same as the height of the initial baseline, indicating that there is no memory leak.

  • memory

Here is an explanation. The blue bar at the beginning of the picture is because I refreshed the page after recording and can be ignored; then we clicked the button and saw that a blue bar appeared again, which is

fn1
Variables in the function
largeObj
Allocated memory,
3s
After that, the memory was released again, which turned into a gray column. So we can conclude that there is no memory leak problem in this code

To summarize briefly: Everyone usually uses the timer, if you do not use the timer, you must clear it, otherwise the situation in this example will occur. apart from

setTimeout
with
setInterval
, In fact, the browser also provides an API, there may be such a problem, that is
requestAnimationFrame

Summary

In the course of the project, if you encounter some performance problems that may be related to memory leaks, you can refer to the list in this article

5
To investigate this situation, you will be able to find the problem and give a solution.

although

JavaScript
The garbage collection is automatic, but sometimes we also need to consider whether to manually clear the memory usage of certain variables. For example, if you know that a variable is no longer needed under certain conditions, it will also be referenced by external variables, resulting in memory failure When released, you can use
null
Re-assign the variable to release the memory of the variable in the subsequent garbage collection phase.

I also thought of writing such an article because the page was stuck due to a memory leak once in my business. Of course it s not that page jams are all caused by memory leaks, there may be other reasons

If you have any suggestions or questions about memory leak troubleshooting methods, you can discuss ducks in the comment section~

Original is not easy, remember

like
Support it~