Summary of knowledge points of Vue first screen performance optimization component

  • 2021-12-05 05:18:13
  • OfStack

Vue first screen performance optimization component

Simple implementation of an Vue first screen performance optimization component, Modern browsers provide many new interfaces, Without considering the compatibility of IE, these interfaces can greatly reduce the workload of writing code and do some performance optimization. Of course, in order to consider IE, we can also cover it when encapsulating components. The first screen performance optimization components in this paper mainly use IntersectionObserver and requestIdleCallback interfaces.

Describe

Consider the first screen scene first, As a first screen mainly for display, Usually, more resources are loaded, such as pictures. If we don't want to load all resources when the user opens it, but want the user to scroll to the relevant position before loading components, we can choose IntersectionObserver interface at this time, and of course we can also use onscroll event to do one listening, but the performance may be worse. There are also some components, which we hope must be loaded, but we don't want them to load synchronously when initializing the page, so we can use asynchronous methods such as Promise and setTimeout, but if we want to reduce the loading priority of this component, we can consider requestIdleCallback interface, and the related code is in vue-first-screen-optimization branch of https://github.com/WindrunnerMax/webpack-simple-environment.

IntersectionObserver

IntersectionObserver interface, which belongs to Intersection Observer API, provides a method to asynchronously observe the intersection state of target element and its ancestor element or top-level document window viewport. Ancestor element and window viewport are called root root, that is to say, IntersectionObserver API, which can automatically observe whether the element is visible. Because the essence of visible visible is that the target element and viewport produce an intersection area, this API is called cross observer, and compatibility https://caniuse. com/? search = IntersectionObserver.


const io = new IntersectionObserver(callback, option);

//  Begin to observe 
io.observe(document.getElementById("example"));
//  Stop observing 
io.unobserve(element);
//  Close the viewer 
io.disconnect();
Parameter callback, after a new IntersectionObserver object is created, executes the specified callback function when it listens that the visible portion of the target element crosses one or more thresholds thresholds. Parameter option, the second parameter of the IntersectionObserver constructor is a configuration object that can set the following properties: threshold attribute determines when to trigger the callback function. It is an array, and each member is a threshold value. The default is [0], that is, when the cross ratio intersectionRatio reaches 0, the callback function will be triggered. Users can customize this array, such as [0, 0.25, 0.5, 0.75, 1], which means that when the target element is 0%, 25%, 50%, 75% and 100% visible, it will trigger the callback function. The root attribute specifies the container node where the target element is located, that is, the root element. The target element will not only scroll with the window, but also scroll in the container, such as scrolling in the iframe window, so it is necessary to set the root attribute. Note that the container element must be the ancestor node of the target element. The rootMargin attribute defines the margin of the root element, which is used to expand or reduce the size of the rectangle rootBounds, thus affecting the size of the intersection area of intersectionRect. It uses the definition method of CSS, such as 10px 20px 30px 40px, which represents the values of top, right, bottom and left. The property IntersectionObserver. root is read-only and listens for the specific ancestor element element of the object. If no value is passed in or the value is null, the window of the top-level document is used by default. The property IntersectionObserver. rootMargin is read-only, Calculate the rectangular offset added to the root root boundary box bounding box when crossing, The value returned by this property may be different from the value specified when the constructor is called, so it may need to be changed to match the internal requirements. All offsets can be expressed in pixels pixel, px or percentages percentage,%, and the default value is 0px 0px 0px 0px. Attribute IntersectionObserver. thresholds is read-only, a list of thresholds, arranged in ascending order, each threshold in the list is the ratio of the intersecting region of the listener to the boundary region, a notification Notification is generated when any threshold of the listener is crossed, and the default value is 0 if no value is passed in by the constructor. Method IntersectionObserver. disconnect () causes the IntersectionObserver object to stop listening. Method IntersectionObserver. observe () causes IntersectionObserver to start listening for a target element. Method IntersectionObserver. takeRecords (), which returns an array of IntersectionObserverEntry objects for all observation targets. Method IntersectionObserver. unobserve () causes IntersectionObserver to stop listening for a specific target element.

In addition, when the callback function is executed, one IntersectionObserverEntry object parameter is passed, which provides the following information.

time: The time when the visibility changes is a high-precision timestamp in milliseconds. target: The observed target element is an DOM node object. rootBounds: Information for the rectangular area of the root element, which is the return value of the getBoundingClientRect method, and returns null if there is no root element and it scrolls directly relative to the viewport. boundingClientRect: Information about the rectangular region of the target element. intersectionRect: Information about the area where the target element crosses the viewport or root element. intersectionRatio: The visible ratio of the target element, that is, the ratio of intersectionRect to boundingClientRect, is 1 when it is completely visible and 0 or less when it is completely invisible.

{
  time: 3893.92,
  rootBounds: ClientRect {
    bottom: 920,
    height: 1024,
    left: 0,
    right: 1024,
    top: 0,
    width: 920
  },
  boundingClientRect: ClientRect {
     // ...
  },
  intersectionRect: ClientRect {
    // ...
  },
  intersectionRatio: 0.54,
  target: element
}

requestIdleCallback

The requestIdleCallback method can accept 1 function, This function will be called when the browser is idle, This enables developers to perform background and low-priority work on the main event loop without affecting delaying key events such as animation and input response. Function 1 will be executed in the order of first-in-first-call. If the callback function specifies the execution timeout time timeout, it is possible to upset the execution order in order to execute the function before timeout. Compatibility https://caniuse.com/? search = requestIdleCallback.


const handle = window.requestIdleCallback(callback[, options]);
The requestIdleCallback method returns an ID, which can be passed into the window. cancelIdleCallback () method to end the callback. Parameter callback, a reference to the function to be called when the event loop is idle, receives a parameter named IdleDeadline, which gets the status of the current idle time and whether the callback was executed before the timeout. The parameter options is optional and includes optional configuration parameters with the following properties: timeout: If timeout is specified and there is a positive value, and the callback is not invoked milliseconds after timeout, the callback task will be queued in the event loop, even if doing so may have a negative performance impact.

Realization

In fact, the main point of writing components is to figure out how to use these two main API. First, pay attention to IntersectionObserver, because you need to use dynamic components < component / > We need to load the component asynchronously () = when we pass the value to it > In the form of import ("component"). When listening, we can consider destroying the listener after loading, or destroying it after leaving the visual area, which is mainly a policy issue. When the page is destroyed, Intersection Observer must be carried out to disconnect to prevent memory leakage. Using requestIdleCallback is relatively simple, and you only need to execute the callback function, which is similar to the asynchronous processing of Promise. resolve (). then.

Here is a simple implementation logic. Usually, the use scheme of observer is to use an div to occupy space first, then monitor its occupied container in observer, and load relevant components when the container is in the visual area. Relevant codes are in the vue-first-screen-optimization branch of https://github. com/WindrunnerMax/webpack-simple-environment. Please try to use yarn for installation. You can use yarn. lock file to lock the version and avoid dependency problems. After running with npm run dev, you can see the order in which the four lazy load components created are created in Console, Among them, observer lazy loading of A needs to wait for its loaded page rendering to be completed, Judge that in the visual area, Before loading, The first screen can be seen directly, while the lazy loading of D will only appear after the external container of D needs to slide the scroll bar to appear in the view, that is to say, D components will not be loaded as long as they do not scroll to the bottom. In addition, attrs and listeners can be passed to lazy loading components through component-params and component-events, similar to $attrs and $listeners, so lazy loading components have been simply implemented.


<!-- App.vue -->
<template>
    <div>
        <section>1</section>
        <section>
            <div>2</div>
            <lazy-load
                :lazy-component="Example"
                type="observer"
                :component-params="{ content: 'Example A' }"
                :component-events="{
                    'test-event': testEvent,
                }"
            ></lazy-load>
        </section>
        <section>
            <div>3</div>
            <lazy-load
                :lazy-component="Example"
                type="idle"
                :component-params="{ content: 'Example B' }"
                :component-events="{
                    'test-event': testEvent,
                }"
            ></lazy-load>
        </section>
        <section>
            <div>4</div>
            <lazy-load
                :lazy-component="Example"
                type="lazy"
                :component-params="{ content: 'Example C' }"
                :component-events="{
                    'test-event': testEvent,
                }"
            ></lazy-load>
        </section>
        <section>
            <div>5</div>
            <lazy-load
                :lazy-component="Example"
                type="observer"
                :component-params="{ content: 'Example D' }"
                :component-events="{
                    'test-event': testEvent,
                }"
            ></lazy-load>
        </section>
    </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import LazyLoad from "./components/lazy-load/lazy-load.vue";
@Component({
    components: { LazyLoad },
})
export default class App extends Vue {
    protected Example = () => import("./components/example/example.vue");

    protected testEvent(content: string) {
        console.log(content);
    }
}
</script>

<style lang="scss">
@import "./common/styles.scss";
body {
    padding: 0;
    margin: 0;
}
section {
    margin: 20px 0;
    color: #fff;
    height: 500px;
    background: $color-blue;
}
</style>
Copy
<!-- lazy-load.vue -->
<template>
    <div>
        <component
            :is="renderComponent"
            v-bind="componentParams"
            v-on="componentEvents"
        ></component>
    </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class LazyLoad extends Vue {
    @Prop({ type: Function, required: true })
    lazyComponent!: () => Vue;
    @Prop({ type: String, required: true })
    type!: "observer" | "idle" | "lazy";
    @Prop({ type: Object, default: () => ({}) })
    componentParams!: Record<string, unknown>;
    @Prop({ type: Object, default: () => ({}) })
    componentEvents!: Record<string, unknown>;

    protected observer: IntersectionObserver | null = null;
    protected renderComponent: (() => Vue) | null = null;

    protected mounted() {
        this.init();
    }

    private init() {
        if (this.type === "observer") {
            //  Existence `window.IntersectionObserver`
            if (window.IntersectionObserver) {
                this.observer = new IntersectionObserver(entries => {
                    entries.forEach(item => {
                        // `intersectionRatio` Is the visible proportion of the target element, greater than `0` Represents visible 
                        //  There are also implementation strategy issues here   For example, do not unload after loading `observe` And destroy it when it is invisible, etc. 
                        if (item.intersectionRatio > 0) {
                            this.loadComponent();
                            //  Unload it after loading is complete `observe`
                            this.observer?.unobserve(item.target);
                        }
                    });
                });
                this.observer.observe(this.$el.parentElement || this.$el);
            } else {
                //  Direct loading 
                this.loadComponent();
            }
        } else if (this.type === "idle") {
            //  Existence `requestIdleCallback`
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            if (window.requestIdleCallback) {
                requestIdleCallback(this.loadComponent, { timeout: 3 });
            } else {
                //  Direct loading 
                this.loadComponent();
            }
        } else if (this.type === "lazy") {
            //  Existence `Promise`
            if (window.Promise) {
                Promise.resolve().then(this.loadComponent);
            } else {
                //  Degraded use `setTimeout`
                setTimeout(this.loadComponent);
            }
        } else {
            throw new Error(`type: "observer" | "idle" | "lazy"`);
        }
    }

    private loadComponent() {
        this.renderComponent = this.lazyComponent;
        this.$emit("loaded");
    }

    protected destroyed() {
        this.observer && this.observer.disconnect();
    }
}
</script>

1 question per day

https://github.com/WindrunnerMax/EveryDay

Reference

https://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html

https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback


Related articles: