Statement: This article for the nuggets first signing article, unauthorized reproduction is prohibited.

Writing in the front

Think a lot of students see memory leak, heart will jump out directly two words: closure!! If you say anything else, you’ll be silenced. If all you know about memory leaks is closures, you should read this article carefully. Closures can cause memory leaks, but memory leaks aren’t just closures, they’re just one of the leads to memory leaks.

Write a program that runs for a while and then slowly gets stuck and even crashes?

As title, a possible memory leaks in your program, when it comes to memory leaks, suggested that read “hardcore JS” do you really understand the garbage collection mechanism , then this article will compare connect fully, after all, garbage collection and memory leaks is cause and effect, the rubbish recycling what did, no rubbish is not recycled is a memory leak.

In this article, we will introduce the concepts related to memory leaks and some of the problems that cause memory leaks. We will also focus on how to troubleshoot, locate, and fix memory leaks (as you learn). Finally, we will briefly expand the other two concepts of memory balloting and frequent GC of the three front-end memory components.

What is a memory leak

The engine has a garbage collection mechanism, it is mainly for some programs no longer used in the object, to clean up the collection to free up memory.

Does garbage collection collect all objects that are no longer used?

Actually engine although recycling against doing all kinds of optimization to ensure waste to recycle as much as possible, but is not to say that we can completely don’t care about this, we still want to take the initiative to avoid some code for engine do recycling operations, because not all useless object memory can be recycled, that when the object is no longer used memory, When it’s not recycled in time, we call it a Memory leak.

Common memory leaks

Code irregularities, two lines of tears, and let’s look at some common cases that can cause memory leaks.

Improper closures

A closure is a function that is nested inside and returns a function. This is what most people think of as a closure, and well, it is, let’s take a look at some of the descriptions in JS highlight books:

  • JavaScript Advanced Programming: CLOSURES are functions that have access to variables in the scope of another function
  • Authoritative Guide to JavaScript: Technically, all JavaScript functions are closures: they are all objects, and they are all associated with a chain of scope
  • JavaScript you don’t know: Closures occur when a function can remember and access the lexical scope in which it is located, even if the function is executed outside the current lexical scope

As described in the previous three books, closures cover a wide range of areas, so let’s leave the definition of closures out for the moment and take a look at the simplest and most commonly accepted closure example:

function fn1(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log('hahaha')}}let fn1Child = fn1()
fn1Child()
Copy the code

Is this a closure? Did it cause a memory leak?

This is obviously a typical closure, but it doesn’t leak memory because there is no reference to fn1 inside the return function. In other words, the test variable inside fn1 is completely recyclable. Let’s see:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
Copy the code

Is this a closure? Did it cause a memory leak?

Obviously it is also a closure, and since the return function contains a reference to the test variable in function fn2, test is not reclaimed, causing a memory leak.

So what’s the solution?

After the function call, just leave the external reference blank, as follows:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
fn2Child = null
Copy the code

“Reduce the use of closures, closures can leak memory…”

Wake up, this sentence is past tense, its description is not accurate, So should say improper use of closures can cause memory leaks.

An implicit global variable

We know that JavaScript garbage collection is automatic, and every once in a while the garbage collector will find the data that is no longer used and free up the memory space it occupies.

If you look at global variables and local variables, local variables in a function are no longer needed after the function is executed, so the garbage collector recognizes them and frees them. However, for global variables, it is difficult for the garbage collector to determine when they are not needed, so global variables are usually not collected. It is OK to use global variables, but at the same time we should avoid generating some extra global variables, as follows:

function fn(){
  // There is no declaration to create the implicit global variable test1
  test1 = new Array(1000).fill('isboyjc1')
  
  // Inside the function this points to window, creating the implicit global variable test2
  this.test2 = new Array(1000).fill('isboyjc2')
}
fn()
Copy the code

Calling function fn, because there is no statement and function of this problem has caused two additional implicit global variables, the two variables will not be recycled, this kind of situation we want to avoid, we can use in the development of strict mode or through lint check to avoid the occurrence of these situations, thus reducing memory cost.

In addition, in our program, we also can not avoid the use of global variables, these global variables unless cancelled or redistribution is not recycled, it also requires our extra attention, that is to say, when we were in the use of global variables to store data, be sure to use after the empty or redistribution, of course, is very simple, Set it to NULL after use. Especially when using global variables to continuously store a large amount of data in the cache, we must remember to set the storage upper limit and timely clean up, otherwise the data volume is increasing, the memory pressure will also increase.

var test = new Array(10000)

// do something

test = null
Copy the code

Free DOM reference

For performance or code conciseness, we cache references to DOM nodes in our code using variables when doing DOM, but when removing nodes, we should release cached references synchronously, otherwise the stray subtree cannot be freed.

<div id="root">
  <ul id="ul">
    <li></li>
    <li></li>
    <li id="li3"></li>
    <li></li>
  </ul>
</div>
<script>
  let root = document.querySelector('#root')
  let ul = document.querySelector('#ul')
  let li3 = document.querySelector('#li3')
  
  // The entire UL and its children cannot be GC because ul variables exist
  root.removeChild(ul)
  
  // The ul element cannot be GC because the li3 variable refers to the children of ul
  ul = null
  
  // No variable reference is available
  li3 = null
</script>

Copy the code

As shown above, when we cache the DOM node reference with variables and delete the node, if we do not empty the variables referenced by the cache, we still cannot GC and there will be a memory leak.

If we empty the parent node, but the deleted references of the parent node and its child nodes are also cached in variables, then the whole free node tree under the parent DOM node tree cannot be cleaned up, and memory leaks will still occur. The solution is to empty the variables that reference the child node, as shown in the figure below:

Forgetting timer

We often use timers in our programs, namely setTimeout and setInterval. Let’s look at an example:

// Get the data
let someResource = getData()
setInterval(() = > {
  const node = document.getElementById('Node')
	if(node) {
    node.innerHTML = JSON.stringify(someResource))
	}
}, 1000)
Copy the code

The above is a small example of my random copy. The code puts the data into the Node every second, but neither the variables in the callback function nor the callback function itself can be reclaimed until the setInterval ends.

What is the end? So clearInterval is called. If it is not cleared, a memory leak can occur. Not only that, but if the callback function is not collected, then the dependent variables in the callback function cannot be collected either. So in the example above, someResource cannot be recycled.

The same goes for setTiemout, so it’s better to call clearInterval or clearTimeout when no interval or timeout is needed. In addition, RequestAnimationFrame in the browser also has this problem, and we need to use the cancelAnimationFrame API to cancel when it is not needed.

Forgotten event listeners

When the event listener component mounted inside the related event handler, and not take the initiative to remove it when the component destroyed, which is the variable or function is considered need not recycle, if internal reference variable stores a large amount of data, may cause the page too much memory, thus causing unexpected memory leaks.

Let’s use the Vue component as an example, and the React component is the same:

<template>
  <div></div>
</template>

<script>
export default {
  created() {
    window.addEventListener("resize".this.doSomething)
  },
  beforeDestroy(){
    window.removeEventListener("resize".this.doSomething)
  },
  methods: {
    doSomething() {
      // do something}}}</script>
Copy the code

Forgotten listener mode

Listener Pattern As we all know, whether it’s Vue, React, or any other front-end development framework, the listener pattern is very common to implement some messaging, such as EventBus..

When we implement the listener pattern and the related components mounted inside the event handler, and not take the initiative to remove it when the component destroyed, one of the reference variable or function is considered need not recycle, if internal reference variable stores a large amount of data, may cause the page too much memory, this can cause unexpected memory leaks.

Let’s use the Vue component as an example because it’s simpler:

<template>
  <div></div>
</template>

<script>
export default {
  created() {
    eventBus.on("test".this.doSomething)
  },
  beforeDestroy(){
    eventBus.off("test".this.doSomething)
  },
  methods: {
    doSomething() {
      // do something}}}</script>
Copy the code

As above, we just need to clean it up in the beforeDestroy component destruction lifecycle.

Forgotten Map and Set objects

When a Map or Set is used to store an Object, the same as an Object is a strong reference. If the reference is not cleared, the memory will not be automatically reclaimed.

If Map is used, WeakMap can be used in the case that the key is an object. WeakMap object is also used to save the key value pair, and the key is a weak reference (note: WeakMap is only a weak reference for the key), and must be an object, and the value can be any object or original value, because it is a weak reference for the object, does not interfere with Js garbage collection.

If you need to use Set reference object, you can use WeakSet, WeakSet object allows to store the only value of the weak reference of the object, the value of the WeakSet object will not repeat, and can only save the weak reference of the object, also because of the weak reference of the object, will not interfere with THE Js garbage collection.

May need a simple introduction here, talk about a weak reference, our strong reference for first, before we say that the garbage collection mechanism of JS is if we hold on an object reference, then the object will not be garbage collected, the reference here, means the strong reference, and if it is only a weak reference is an object reference by a weak reference, Is considered inaccessible (or weakly accessible) and therefore may be reclaimed at any time.

Don’t understand? Here’s an example:

// obj is a strong reference, the object is stored in memory, available
let obj = {id: 1}

// Override the obj reference
obj = null 
// Remove the object from memory, reclaim the {id: 1} object
Copy the code

Above is a simple way to clear an object reference by overwriting it, making it recyclable.

And look at this:

let obj = {id: 1}
let user = {info: obj}
let set = new Set([obj])
let map = new Map([[obj, 'hahaha']])

/ / rewrite obj
obj = null 

console.log(user.info) // {id: 1}
console.log(set)
console.log(map)
Copy the code

{id: 1} () {id: 1} () {id: 1} () {id: 1} () {id: 1} () {id: 1} 1}, if we want to clear it then we have to override all the references and make them null.

Let’s look at WeakMap and WeakSet:

let obj = {id: 1}
let weakSet = new WeakSet([obj])
let weakMap = new WeakMap([[obj, 'hahaha']])

// Override the obj reference
obj = null

// {id: 1} will be deleted from memory in the next GC
Copy the code

As shown above, WeakMap and WeakSet are used as weak references. After the OBj reference is set to null, object {ID: 1} will be cleared out of memory in the next GC.

Uncleaned Console output

In the process of writing code, certainly can’t avoid some output, in a small team might not clean up these projects launched in the console, but the console is also a hidden danger, at the same time also is easy to be ignored, we can see the console output data, because the browser save our information data output object references, This is why uncleaned console can also cause a memory leak if it outputs objects.

So, in a development environment, you can use console output to help you debug, but in a production environment, it’s important to clean up the output.

There may be students feel incredible, even do not believe, here we leave an example, you can just test after reading the article, can first save this code oh! (Read on to find out how)

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>test</title>
</head>

<body>
  <button id="click">click</button>

  <script>
    !function () {
      function Test() {
        this.init()
      }
      Test.prototype.init = function () {
        this.a = new Array(10000).fill('isboyjc')
        console.log(this)}document.querySelector('#click').onclick = function () {
        new Test();
      }
    }()
  </script>
</body>

</html>
Copy the code

Memory leak detection, location, and repair

As said at the beginning, after running for a period of time, the program slowly changed card and even to crash, do not know what the reason, then we will go through the troubleshooting, locating and fixing the entire process of memory leak through an example, knock on the blackboard, this is really available to everyone.

Since we have mentioned several cases of memory leaks above, let’s use these cases to write a Demo to check whether there is a memory leak from the perspective of the browser. If there is a leak, locate the source and fix it.

First, let’s fake a memory leak example:

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>test</title>
</head>

<body>
  <button id="click">click</button>
  <h1 id="content"></h1>

  <script>
    let click = document.querySelector("#click");
    let content = document.querySelector("#content")
    let arr = []

    function closures() {
      let test = new Array(1000).fill('isboyjc')

      return function () {
        return test
      }
    }

    click.addEventListener("click".function () {
      arr.push(closures())
      arr.push(closures())

      content.innerHTML = arr.length
    });

  </script>
</body>

</html>
Copy the code

As shown above, this is a very simple example of a memory leak caused by improper use of closures.

First of all, we have a closures function, which is a closures function. The simplest closure function must not be introduced to you. Then we bind a click event for the button element in the page. Each click will execute the closure function twice and push its execution result to the global array ARR. Since the closure function execution result is also a function and there is a reference to the test array inside the original closure function, Therefore, each element in the ARR array makes the internal test array object referenced by the arR array unrecyclable. The number of elements in the ARR array also represents how many closure references we have. Therefore, the more clicks and pushes the program, the more memory consumption, and the page will become more and more stuck.

For the sake of later observation, we update the length of the global array arR to the page every time we click the button, that is, starting from 0, each time we click, the page value is increased by 2.

Of course, this is an example we wrote ourselves, as god we know what caused it, so for now, forget about this, let’s say this is one of our project procedures, the development of the completed and delivered to the test, the test sister found that the page in the program after the continuous click of the button, then mentioned the BUG.

As programmers we must be: “refresh the page is not good, card on the refresh refresh!!”

B: well… The e product and test will certainly not allow, a user first let us change.

Ok, then change, first of all, the first step is to identify where the problem is, what caused, then this link we call the problem detection stage.

Troubleshoot problems

Chrome’s developer tools, known as the Chrome Devtool, are a powerful tool that can help you analyze all sorts of things in your application like performance, security, and networking. They can also help you quickly pinpoint the source of problems, but most people aren’t familiar with them.

Since we focus on memory leak in this article, we will default that the above program has checked all items except memory and it is ok. Next, we will start to check memory.

First we open the browser in non-trace mode, then open the web application code to check, and then open the console, the entire application interface is very simple, as shown below:

Then we go to the Performance panel, formerly known as Timeline, which is a great tool for Chrome Devtool to monitor Performance metrics by recording and analyzing events that occur over the life of a website. We can use it to monitor and analyze various performance conditions in our program, including memory, as shown below:

Next, make sure you check the Memory option (mark 5 above) before you start, so that you can see the Memory analysis.

Click Start Recording (mark 1) to enter the recording state, and then clean up the GC by clicking the small trash can (mark 6).

Then we frantically click the click button on the page 100 times, at which point the value on the page should be 200, and click the little trash can again to manually trigger a GC.

Click the click button on the page 100 times again. At this point, the value on the page should be 400, and then stop recording.

Let’s look at the data panel generated by the console, as shown below:

The top two circled in red, the part corresponding to the Heap, indicate that there are periodic falls in the memory, which is simply our memory.

We can clearly see that the memory data is showing a rising trend, some people may say this period of time has not performed GC? Don’t worry, remember when we clicked on the small trash can at 200, that is, we manually triggered garbage collection once, we can find out the moment when the page value of 200 through the page snapshot above, it is very simple, mouse over to the memory snapshot to find, as shown in the figure below:

As you can see, even if we manually do a garbage collection operation, the memory after cleaning is not much reduced, so we infer that the click operation of this program may have a memory leak.

OK, we’ve identified the problem, so the next step is to locate the source of the leak.

You might say, well, since you’ve identified the problem with the click event, why don’t you just go ahead and fix it?

You know, this is we write a simple example, we can see where the problem, but a click event in real project might exist a large number of operations, and we only know that the click event may lead to memory leaks, but don’t know the specific problem is on the click event which steps, cause more fine-grained and location we don’t know, All these need further analysis to locate.

Analysis of the positioning

Next, we began to analyze and locate the source of the leak

Chrome Devtool also provides a Memory panel, which provides more detailed information such as recording JS CPU execution time details, displaying Memory consumption of JS objects and associated DOM nodes, recording Memory allocation details, and so on.

Heap Profiling is a snapshot of the current Heap memory and generates a description file of the object. The description file shows all the objects used in JS running at the moment, as well as the size of the memory occupied by these objects, the hierarchy of references, and so on. It can be used to pinpoint the exact cause and location of the problem.

Note that this is not the Memory under the Performance panel, but the Memory panel at the same level as the Performance panel, as shown below:

Now the page value is 400, but of course you can refresh the page and start at 0, so we’re going to continue

First, click on the small trash can (mark 3) to trigger a GC to flush out the useless stuff from memory

Click Start Generate Snapshot (mark 1) to generate the first snapshot and select it, as shown below:

A brief description of what the small picture roughly means:

Snapshot 1 in the list on the left represents Snapshot 1 that we generated, which is the memory state at that moment

Select Snapshot 1 to create the right view table with a drop-down box at the top left with four values

  • Summary: Grouping objects by constructor and capturing their memory usage can be understood as a memory Summary for tracking memory leaks that locate DOM nodes
  • Comparison: Compares the memory snapshot differences before and after an operation and analyzes the memory release before and after the operation to determine whether memory leaks exist and the cause
  • Containment: Probes the specifics of the heap and provides a view into the structure of the objects, which helps analyze object references, closures, and deeper object analysis
  • Statistics: Statistics view

This drop-down selects Summary for us by default, so the table below shows the memory Summary of the data in snapshot 1. The table below shows the memory Summary of the data in snapshot 1, including the memory usage information and so on.

Just to get a sense of what the Summary option data table columns represent

  • Constructor: Displays all constructors. Click on each Constructor to see all objects created by that Constructor
  • Distance: Shows the Distance to the root node via the shortest node path, referring to the hierarchy
  • Shallow Size: Displays the memory occupied by the object, not by other objects that contain internal references
  • Retained Size: displays the total memory used by the object, including the memory used by other internally referenced objects

OK, that’s all we know for now, let’s continue to operate, first click the small trash can to manually perform a GC, then click the click button on the next page of 1, and finally click the generate snapshot button again to generate our second snapshot.

To get it right, let’s do it a few more times, as follows:

First click the small trash can to manually perform a GC, then click the Click button on the next page of 2, and finally click the Generate snapshot button again to generate our third snapshot

First click the small trash can to manually perform a GC, then click the click button on the next page of 3, and finally click the generate snapshot button again to generate our fourth snapshot

Next, we select snapshot 2 and switch the drop-down box above it from the default Summary option to comparison, which compares the memory of the current snapshot with that of the previous snapshot, as shown below:

Let’s take a look at what the table columns below represent after selecting the Comparison drop-down. Here are some important ones

  • New: How many objects are created
  • Deleted: Indicates the number of objects reclaimed
  • Delta: number of newly created objects minus number of reclaimed objects

We need to focus on Delta, as long as it’s positive, it can be a problem. The nice console has sorted it out for us, so let’s look at the top ones in turn.

, of course, we also need to know the each line of data is representative of what is, attention to the Constructor of this column, we also said that one of them is a Constructor, each Constructor click can view all the objects created by the Constructor, or want to introduce this column in common Constructor is roughly represent

  • System and System /Context represent references created by the engine itself and by the Context. These are not important
  • Closure represents an object reference in some function closure
  • The array, string, Number, and regexp series can also be seen as object types that reference arrays, strings, numbers, or regular expressions
  • HTMLDivElement, HTMLAnchorElement, DocumentFragment, and so on are all references to elements in your code or to specified DOM objects

Well, that’s a lot clearer, so then we can compare 1-> 2/2 -> 3/3 ->4 to see what’s wrong.

Don’t worry. What are we going to do now? You need to click on a single snapshot and then select Comparison, then look at the positive Delta column and then analyze, which takes three times, because you have four snapshots, and you need to compare three times, and sometimes even more snapshots to ensure accuracy.

Is there an easier way? Yes, we can directly select the snapshot to be compared. There is also a box on the right side of the table for us to directly select the snapshot for comparison, and it will kindly filter out some useless information for us:

Let’s perform the actual operation. Select snapshot 2 on the left, and select snapshot 1 and snapshot 2 for comparison analysis. The results are as follows:

As you can see, there are only four comparison filtered differences left in the list

System /Context we don’t have to worry about.

Closure refers to a closure reference. Click on this item to see more details:

As you can see, there are two references under closure, and it kindly points out that in line 21 of the code, click on one of the references and further details are displayed below.

Why are there two references after expansion? Remember what we did when we generated snapshot 2, we manually performed a GC and clicked the click button once, triggering a click event, and in the click event we executed and pushed the closure twice, so that’s 2 records.

Finally, we look at array. There is a reference to array because of the global array variable arR in our case code. After all, we push the data every time we click on it. Because like this kind of situation you don’t clean up these global variables in have the page in memory, may everyone to have 2 items in the constructor columns are array in doubt, it is nothing wrong with, is a representative of arr itself, is a representative of the closure of internal reference array variable test (forget to review the case code above), This can also be got through the reference hierarchy represented in the Distance column, one of which is 7 and one of which is 8. As for the location of the code that caused the leak in the array, we can also click expand and select the reference item, and see the code location in the details, the same operation as the closure above, but we will not demonstrate it here.

If this is true, let’s check again. Select snapshot 4 on the left and compare snapshot 3 with snapshot 4. What we did before snapshot 4 was to manually perform a GC and click the button three times. This should be the same as our comparison of snapshot 1 and snapshot 2, which are all 4 items, but the internal reference to each item will be 6, because we clicked the button three times before the snapshot, executing and pushing the closure function twice each time. Here is the result:

Well, at this point it seems clear that there are two problems. One is the memory leak caused by the closure reference to the array in line 21 of code, and the other is the memory leak caused by the increasing number of elements in the global variable arr.

If the fault is located successfully, go to the next step to repair and verify the fault again.

Repair verification

Since this is a case written temporarily and there is no specific scenario, there is no way to fix it in a targeted way. So, this step is temporarily ignored, but we still need to solve it in the project.

For example, the global object keeps increasing, we can not avoid the global object, but we can limit the size of the global object, according to the scene can exceed the part of the clean up.

Such as the closure reference problem, not allowing it to reference, or performing null, which is all mentioned above.

In short, everything needs to be selected according to the specific scenario, after the solution to repeat the above troubleshooting process can see the memory.

Memory three large

There are actually three main problems with memory on the front end, which I affectionately call the memory Three:

Memory leaks we’ve been talking about for a long time, objects that are no longer used but are not reclaimed, memory that is not freed, memory leaks, so the way to avoid that is to avoid having useless data still being referenced, so pay more attention to the common memory leaks that we mentioned above.

To avoid memory ballooning, you need to use technical measures to reduce the memory footprint.

Frequent GC is the same name, which means that GC is executed very frequently, usually when a large temporary variable is used frequently and the new generation fills up very quickly, and every time the new generation fills up, the GC is triggered. Frequent GC also causes pages to stall. To avoid this, don’t have too many temporary variables. Because temporary variables will be recycled when they are not used, which is similar to what we said in the memory leak to avoid using global variables. In fact, as long as the degree is good, not too much is OK.

The last

If your program runs slow but can’t find the reason, it might as well try the troubleshooting method in this article, it may be caused by a memory leak, at the same time, it is also a point we need to pay extra attention to when doing page optimization.

That’s all for today. Did you GET it? Welcome! Writing is not easy, move a small hand point like it, collecting eating ash is taboo 👊

Also welcome to the “hardcore JS” column, an in-depth introduction to a JS little knowledge, let you know you do not know JavaScript!!