If you know what is, you will know why. In a world full of wheels, even if we don’t have to build our own wheels, we still need to understand how the wheel works in order to make others’ wheels ours.

Why lazy loading

In normal development, we always encounter long lists, because the Web itself does not perform very well on long lists; In addition, the Web itself is particularly affected by network fluctuations, and too much content loaded on the first screen at the same time will lead to problems such as stuttering and slow response. Lazy loading is often used to optimize this.

What is lazy loading

Lazy loading, also known as lazy loading, refers to lazy loading of the DOM on long web pages (often used in jquery days for lazy loading of images, but now also for lazy loading of complex components) and is one way to optimize web performance. When the DOM is not in the viewable area, the DOM is displayed using placeholders, and when it reaches the viewable area, the actual DOM is loaded and rendered.

How to implement lazy loading

As browsers become more powerful, there are two ways to implement lazy loading these days;

  1. Use the Listen scroll event to listen
    • Advantages: Good compatibility;
    • Disadvantages: Complex implementation; Large amount of calculation and poor performance;
  2. IntersectionObserver is used for monitoring
    • Advantages: simple implementation; High performance;
    • Disadvantages: Poor compatibility (polyfill can be used); The API is asynchronous and does not fire synchronously with the scrolling of the target element. According to the specification, IntersectionObserver should be implemented using requestIdleCallback(), that is, only when the thread is idle will the observer be executed. This means that the observer has a very low priority and only executes when the browser is free after other tasks have been executed.

In this case, SCROLL is adopted for implementation because IntersectionObserver is asynchronous, so it is easier to implement IntersectionObserver with Scroll in consideration of the problem of anti-shake throttling. The idea is to listen for events and then perform methods that check if the elements are visible and then perform tasks. It is mentioned here that anti-shake and throttling can effectively improve performance by using anti-shake and throttling when the user quickly slides through the long list, but the view is not viewed by the user. Here we briefly explain the difference between anti-shake and throttling

  • Anti – shake: only perform the last time when triggered several times within the specified time
  • Throttling: To execute only a certain number of times when triggered multiple times within a specified period of time

The purpose of anti-shake and throttling is to limit the execution frequency of the function, so as to optimize the phenomenon of lagging response speed, suspended animation or stalling caused by the function triggering frequency too high

Anti – shake function: the principle is to maintain a timer, after a specified time to perform the callback. If triggered again during this period, the timer restarts

function debounceFunc(fn: any, wait: number) {
    let timer:any = null;
    return function () {
        let args = arguments;

        timer && clearTimeout(timer);

        timer = setTimeout(() = > {
            //@ts-ignore
            fn.apply(this, args) }, wait); }}Copy the code

Throttling function: The principle is to determine whether the specified time has been reached, when the callback is performed

function throttleFunc(fn: any, wait: number) {
    let time = 0.timer:any = null;
    return function () {
        let now = Date.now();
        let args = arguments;
        if (now - time > wait) {
            //@ts-ignore
            fn.apply(this, args);
            time = now;
        } else {
            timer && clearTimeout(timer);
            timer = setTimeout(() = > {
                //@ts-ignore
                fn.apply(this, args); time = now; }, wait); }}}Copy the code

The core content

As mentioned above, the core of lazy loading is to detect whether the element is visible, detect whether the element is visible and determine whether the DOM position is in the visible area, mainly by top, left. We can use the getBoundingClientRect method to obtain the specific information of the DOM. Here we try to implement a checkVisible function using JS.

// Define a function that takes the DOM to check and the scrolling container DOM. Returns a Boolean
const checkVisible = (dom: HTMLElement, parentDom: HTMLElement): boolean= > {
    // Get dom information
    const { top, left, width, height } = dom.getBoundingClientRect();
    const {
        top: parentTop,
        left: parentLeft,
        width: parentWidth,
        height: parentHeight
    } = parentDom.getBoundingClientRect();
    // Get the screen width and height
    const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight;
    const windowInnerWidth = window.innerWidth || document.documentElement.clientWidth;
    
    const intersectionTop = Math.min(parentTop, 0);
    const intersectionLeft = Math.min(parentLeft, 0);
    // Calculate the viewable area height and width, since parentDom may be larger than the screen, take the maximum width of window
    const intersectionHeight = Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop;
    const intersectionWidth = Math.min(windowInnerWidth, parentLeft + parentWidth) - intersectionLeft;
    // Calculate the distance between the DOM and parentDom to check
    const offsetTop = top - intersectionTop;
    const offsetLeft = left - intersectionLeft;
    // Make a comparison
    return (
        offsetTop <= intersectionHeight &&
        offsetTop + height >= 0 &&
        offsetLeft <= intersectionWidth &&
        offsetLeft + width >= 0)}Copy the code

Now that we’ve done the core function, we’re going to convert it to react

API design

parameter instructions type The default value
children Mandatory. Lazy component loading React.ReactNode
loading Mandatory, placeholder component React.ReactNode
scrollContainer Optional, scroll the container string/dom document.body
offset Optional, offset number/Array(number) 0
resize Optional: Whether to listen for resize events boolean false
debounce Optional, anti – shake time, priority over throttling number 0
throttle Optional, throttling time, priority lower than anti – shake number 0

The source code parsing

import React, { useState, useRef, useEffect } from 'react';
// Define component Props
interface LazyloadProps {    
    loading: React.ReactNode; scrollContainer? : HTMLElement; offset? :number; resize? :boolean; debounce? :number; throttle? :number;
}
// The event to listen on
const DEFAULT_EVENTS = [
    'scroll'.'wheel'.'mousewheel'.'animationend'.'transitionend'.'touchmove',];const Lazyload: React.FC<LazyloadProps> = (props) = > {
    // Set the Props default value
    const {
        children,
        loading,
        scrollContainer = document.body,
        offset = 0,
        debounce = 0,
        throttle = 0,
        resize = false
    } = props;
    // Whether it is visible
    const [isVisible, setVisible] = useState(false);
    // Wrap the container
    const containerRef = useRef<HTMLDivElement>(null)

    useEffect(() = > {
    	// Check the function
        let checkVisible = () = > {
            // Do not count if the container does not exist
            if(! containerRef.current)return;
            // Get the current component location
            const{ top, left, width, height } = containerRef.current? .getBoundingClientRect();// Get the scroll container position
            const {
                top: parentTop,
                left: parentLeft,
                width: parentWidth,
                height: parentHeight
            } = scrollContainer.getBoundingClientRect();
            // Calculate the screen height and width
            const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight;
            const windowInnerWidth = window.innerWidth || document.documentElement.clientWidth;
            // Calculates the intersection of the scroll container and the screen viewable area
            const intersectionTop = Math.min(parentTop, 0);
            const intersectionLeft = Math.min(parentLeft, 0);

            const intersectionHeight = Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop;
            const intersectionWidth = Math.min(windowInnerWidth, parentLeft + parentWidth) - intersectionLeft;
            // Calculate the height of the component from the viewable area
            const offsetTop = top - intersectionTop;
            const offsetLeft = left - intersectionLeft;
            [top, left]
            const offsets = Array.isArray(offset) ? offset : [offset, offset];
            // Determine whether the component is in the visible area by the above distance
            const isVisible = offsetTop - offsets[0] <= intersectionHeight &&
            offsetTop + height + offsets[0] > =0 &&
            offsetLeft - offsets[1] <= intersectionWidth &&
            offsetLeft + width + offsets[1] > =0;

            setVisible(isVisible);
            // Cancel all listening for the current component if visible
            if (isVisible) {
                DEFAULT_EVENTS.forEach((event) = >{ scrollContainer? .removeEventListener(event, checkVisible); });window.removeEventListener('resize', checkVisible); }}// Enhance the checkVisbile function with anti-shake throttling, which takes precedence over throttling
        if (debounce) {
            // The buffeting function
            checkVisible = debounceFunc(checkVisible, debounce);
        } else if (throttle) {
            // throttling function
            checkVisible = throttleFunc(checkVisible, throttle);
        }
        // Listen for events in batches
        DEFAULT_EVENTS.forEach((event) = >{ scrollContainer? .addEventListener(event, checkVisible); });if (resize) {
            window.addEventListener('resize', checkVisible);
        }
        // Automatically execute once
        checkVisible();

        return () = > {
            DEFAULT_EVENTS.forEach((event) = >{ scrollContainer? .removeEventListener(event, checkVisible); })window.removeEventListener('resize', checkVisible);
        }
    }, [scrollContainer, containerRef.current])
    
    return (
        <div ref={containerRef}>
            { isVisible ? children : loading }
        </div>)}Copy the code

How to use

npm install @lumu/lazyload –save

import React from 'react';
import Lazyload from '@lumu/lazyload';

const Loading = () = > {
    return (
        <img
            className="test"
            alt=""
            src={require('./loading.gif')} / >)}const App = () = > {
    return (
        <React.Fragment>
            {
                new Array(10).fill(1).map((_, index) => (
                    <Lazyload
                        resize
                        scrollContainer={document.getElementById('root') as HTMLDivElement}
                        debounce={300}
                        offset={50}
                        loading={<Loading/>}
                        key={index}>
                        <img alt="" src="https://img01.yzcdn.cn/vant/apple-1.jpg" width="100%" height="300"/>    
                    </Lazyload>))}</React.Fragment>)}Copy the code

The source address

Example demonstrates

Review past

  • Disassemble the React component step by step –Swipe scroll diagram

The last

Think it works? Like to collect, by the way point like it, your support is my biggest encouragement! Think it’s useless? Comment section to share your ideas, open to your guidance.