What is a memory leak? A memory leak is a new block of memory that cannot be released or garbage collected. When an object is new, it claims a chunk of heap memory. When the object pointer is set to null or the object is destroyed out of scope, the chunk of memory is no longer referenced and is automatically garbage collected in JS. However, if the object pointer is not set to NULL and the code can no longer retrieve the object pointer, it will not be able to free the memory it points to, which is a memory leak. Here’s an example of why this object pointer is not available in code:

// module date.js
let date = null;
export default {
    init () {
        date = new Date();
    }
}

// main.js
import date from 'date.js';
date.init();Copy the code

After main.js initializes date, the date variable will remain in place until you close the page, because the reference to date is in another module and is invisible to the outside world. So if you want the date object to be in there all the time and need to use it all the time, that’s fine, but if you want to use it all the time and you don’t want to use it all the time, that’s a problem, because the object is in memory and it hasn’t been freed and it leaks.

Another insidious and common memory leak is event binding, which creates a closure that causes some variables to persist. See the following example:

// An example of an image lazy loading engine
class ImageLazyLoader {
    constructor ($photoList) {
        $(window).on('scroll', () = > {this.showImage($photoList);
        });
    }
    showImage ($photoList) {
        $photoList.each(img= > {
            // Load the image as soon as it slides out
            img.src = $(img).attr('data-src'); }); }}// Initialize an image lazily loaded when clicking on a page
$('.page').on('click'.function () {
    new ImageLazyLoader($('img.photo'));
});Copy the code

This is a lazy loading model of images. Each time a page is clicked, the data on the previous page is cleared and updated to the DOM of the current page, and a lazy loading engine is reinitialized. It listens for the Scroll event to process the DOM of the list of images that are passed in. Every time a page is clicked, a new page is created. This is where a memory leak occurs, mainly due to the following three lines of code:

$(window).on('scroll', () = > {this.showImage($photoList);
});Copy the code

This is an instance of ImageLazyLoader, and $photoList is a DOM node. When the last page of data is cleared, The associated DOM nodes have been separated from the DOM tree, but there is still a $photoList pointing to them, so the DOM nodes cannot be garbage collected and remain in memory, causing a memory leak. There is also an instance of ImageLazyLoader that is leaking because the this variable is also trapped in the closure and is not released.

The solution to this problem is simply to disable the bound event while destroying the instance, as shown in the following code:

class ImageLazyLoader {
    constructor ($photoList) {
        this.scrollShow = (a)= > {
            this.showImage($photoList);
        };
        $(window).on('scroll'.this.scrollShow);
    }
    // Add an event unbind
    clear () {                     
        $(window).off('scroll'.this.scrollShow);
    }
    showImage ($photoList) {
        $photoList.each(img= > {
            // Load the image as soon as it slides out
            img.src = $(img).attr('data-src');
        });
        // If all images are displayed, unbind the event
        if (this.allShown) {
            this.clear(); }}}// Initialize an image lazily loaded when clicking on a page
let lazyLoader = null;
$('.page').on('click'.function () {
    lazyLoader && (lazyLoader.clear());
    lazyLoader = new ImageLazyLoader($('img.photo'));
});Copy the code

In each instantiation of a ImageLazyLoader before the first instance of the last clear off, clear inside unbinding, because JS has a constructor but no deconstruction function, so you need to write a clear, in the outside manually adjust clear. At the same time, the event is automatically unbound at the appropriate time during the execution of the event, and the top is to say if all the images are displayed then there’s no need to listen for scroll events and just unbind them. This will solve the memory leak problem and trigger automatic garbage collection.

Why should there be no closure references if the event is unbound? Because the JS engine detects that the closure is no longer useful, it destroys the closure, and the external variables referenced by the closure are naturally empty.

Ok, the basic knowledge is explained here, now use Chrome DevTools memory detection tool to actually operate again, convenient to find some page memory leakage behavior. To avoid the effects of plugins installed in the browser, use Chome’s incognito mode page, which disables all plugins.

Then open devTools, cut to the Memory TAB, and select Heap Snapshot, as shown below:

What is heap snapshot? Take a picture of the current heap. Because dynamically allocated memory is stored in the heap, local variables are stored in the stack and are allocated and managed by the operating system, there is no memory leak. So just worry about the heap.

Then do some DOM additions, deletions, and changes, such as:

(1) Play a box, and then turn off the box

(2) Click on a single page to jump to another route, and then click back to return

(3) Click paging to trigger dynamic DOM change

Add the DOM and delete the DOM to see if any objects reference the deleted DOM.

Here I am using the second scenario to detect memory leaks in a routing page of a single-page application. Open the home page, go to another page, click Back, and click the Garbage Collection button:

Trigger garbage collection to avoid unnecessary distractions.

Then click the photo button again:

It scans the heap of the current page and displays it, as shown below:

Look for detached in the Class Filter search box in the middle above:

It displays all DOM nodes that have been separated from the DOM tree, focusing on the non-empty distance, which represents the distance from the DOM root. What are the divs shown above? If we hold the mouse for 2 seconds, it will display the DOM information for the div:

It can be seen from className and other information that it is the DOM node of the page to be checked. In the following Object window, expand its parent node successively, and you can see that its outermost parent node is a VueComponent instance:

The yellow font native_bind indicates that an event is pointing to it, and yellow indicates that the reference is still valid. Hover the mouse over native_bind for 2 seconds:

It will prompt you that the file in homework-web.vue has a getScale function bound to the window. Check that the file does have a binding:

mounted () {
    window.addEventListener('resize'.this.getScale);
}Copy the code

So even though the Vue component removes the DOM, there is still a reference, so the component instance is not freed, and there is a $EL in the component that points to the DOM, so the DOM is not freed.

But look at the code and unbind it in beforeDestroyed:

beforeDestroyed () {
    window.removeEventListener('resize'.this.getScale);
}Copy the code

So there should be no problem?

The name of the function was wrong. It should be:

beforeDestroy () {
    window.removeEventListener('resize'.this.getScale);
},Copy the code

Found a bug that has been hidden for several days, because it is relatively hidden, even if it is wrong, there will be no obvious perception.

Change this area, repeat the process, and take another memory snapshot. We find that the number of free div nodes is still 74 and disance is not empty, as shown in the figure below:

Didn’t you just change it right? Continue with the second node:

GToNextHomworkTask = gToNextHomworkTask = gToNextHomworkTask = gToNextHomworkTask If we search where the event is bound, we can find that it is bound in a child of the routing component:

mounted () {
    EventBus.$on('goToNextHomeworkTask'.this.go2NextQuestion);
}Copy the code

Sure enough, the component only has $on and no $off, so there is still a reference to the event when the component is unloaded. So you need to remove $off from the component’s destroyed:

destroyed () {
    EventBus.$off('goToNextHomeworkTask'.this.go2NextQuestion);
}Copy the code

Refresh the page for the third time and then take a memory snapshot. The embarrassing situation is the same:

Note that someone else has referenced it, continue to check who has referenced it without releasing:

It can be found that a Vuex $store watch listener is not released. The cb attribute of Watcher can be used to know the specific listener function. Using a simple text search, we found that watch was executed in a sub-component:

mounted () {
    this.$store.watch(state= > state.currentIndex, (newIndex, oldIndex) => {
        if (this.$refs.animation && newIndex === this.task.index - 1) {
            this.$refs.animation.beginElement(); }}); }Copy the code

There is a this pointer in watch that points to the DOM element of the component. Since the child component is not released, the parent component containing it will not be released naturally, so one layer after another, the outermost routing component will not be released either.

This should be unwatched during destroyed:

mounted () {
    this.unwatchStore = this.$store.watch(state= > state.currentIndex, (newIndex, oldIndex) => {
        / / code slightly
    }); 
},
destroyed () {
    this.unwatchStore();
}Copy the code

After processing, take a memory snapshot, as shown below:

Although there are still 74, distance is empty, but distance is not empty compared with the previous three steps. In addition, the part marked yellow is not found in the Object expansion below, which means that the problem of memory leakage of this routing component has been solved.

Let’s continue to look at other div nodes where distance is not empty, as shown in the figure below, which can be sorted by distance:

One of them is. Animate -container:

It is a DOM container for Lottie animations, and Lottie objects still have references to it:

This is a loading animation made by Lottie. When loading ends, I manually switch its Stop API to stop the animation and remove the animte-Container. However, Lottie still refuses to let go of it. My code looks like this:

let loadingAnimate = null;
let bodymovinAnimate = {
    // Display loading animation
    showLoading () {
        loadingAnimate = bodymovinAnimate._showAnimate();
        return loadingAnimate;
    },
    // Stop loading animation
    stopLoading () {
        loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
    },
    // Start Lottie animation
    _showAnimate () {
        const animate = lottie.loadAnimation({
            // The argument is omitted
        }); 
        return animate;
    }
    // End Lottie animation
    _stopAnimate (animate) {
        animate.stop();
        let $container = $(animate.wrapper).closest('.bodymovin-container'); $container.remove(); }};export default bodymovinAnimate;Copy the code

I guess Lottie didn’t release the DOM reference after stop, because stop is still enough to restart the animation, so it has to bite on the DOM. So if you want to end the animation completely, instead of stop, Lottie also has a destroy method. Replace stop with destroy:

    // End Lottie animation
    _stopAnimate (animate) {
        animate.destroy();
        let $container = $(animate.wrapper).closest('.bodymovin-container');
        $container.remove();
    },Copy the code

When this is done, Lottie’s reference releases it, the problem is resolved, and a new photo is taken:

There is still an exports.default pointing to it, which is a module of Webpack. I guess because of the example mentioned at the start of this article, the module formed a closure and its variables were not released to cause memory leaks, so I set it to null in stopLoading:

    // Stop loading animation
    stopLoading () {
        loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
        loadingAnimate = null;
    },Copy the code

After doing this, the.animate-container DOM object is no longer referenced.

Finally, there are three remaining divs with distances:

Two of them are the jq $. Support. BoxSizingReliable, jq is used to detect whether boxszing created divs are available:

Here’s another one from Vue:

These are memory leaks caused by the libraries used, so forget about them for now.

The same thing happens with other labels.

Therefore, based on the above analysis, memory leakage may be caused by the following situations:

(1) Listen on events such as window/body are not unbound

(2) Events tied to EventBus are not untied

(3) There is no Unwatch after Vuex’s $Store Watch

(4) The internal variables of the closure formed by the module are not set to NULL after use

(5) Created using a third-party library without calling the correct destruction function

In addition, Chrome’s memory analysis tool can be used for quick investigation. This paper mainly uses the basic function of memory heap snapshot. Readers can try to analyze whether their page has memory leakage by doing some operations such as flicking a box and closing it, taking a snapshot of the heap, searching detached, sorting by distance, Expand the parent of the non-empty node and find the words marked yellow indicating that there are unfreed references. In other words, this method mainly analyzes the free DOM nodes that still have references. Since memory leaks on pages are usually DOM related, regular JS variables are generally not a problem due to garbage collection, unless they are trapped by closures and not empty.

Dom-related memory leaks are also often caused by closures and event bindings. Once the (global) event is bound, it needs to be unbound when it is not needed. Of course, div can be directly tied to the div can be directly deleted, tied to the event will be untied.

The Efficient Front End has been printed for the second time