别再让用户等哭!React 懒加载:让组件 “躺平” 到要用时再干活

310 阅读5分钟

前言

之前咱们聊了普通网页的图片懒加载 —— 让图片 “藏” 在屏幕外摸鱼,滚到眼前再开工。那 React 项目里的组件和图片呢?总不能一打开页面就把所有组件的代码都拽过来吧?

想象下:你打开购物App页面,首页就加载了 “订单页”“个人中心” 的代码,结果用户根本没点进去 —— 这不是纯纯的浪费流量和性能吗?React 的懒加载,就是给组件和图片发 “摸鱼许可证”没出现在视野里,就别加载,躺平到要用时再干活

如果不了解懒加载的,我的上篇文章在这:

救命!我那加载慢到离谱的图片,终于被懒加载 “救活” 了

一、React 里的 “组件懒加载”:React.lazy + 动态导入

我们先创建一个Demo组件,并看这行代码:

// 别直接import Demo,改成动态导入 
const Demo = lazy(() => import('./Demo'));

这行代码的魔力在于:

  • 原本import Demo from './Demo'会在页面初始化时就加载 Demo 组件的代码
  • React.lazy+import()包裹后,Demo 的代码会被单独打包成一个小文件,只有当 Demo 组件要被渲染(比如滚到可视区域)时,这个小文件才会被下载、加载。

搭配Suspense(不然组件加载时会 “崩”)

不过光用lazy还不够 —— 组件加载需要时间,加载过程中页面会报错。得用Suspense当 “占位符”:

import { lazy, Suspense } from 'react';
const Demo = lazy(() => import('./Demo'));

export default function App() {
    return (
        <div>
            {/* 一堆内容... */}
            <Suspense fallback={<div>组件加载中...</div>}>
                <Demo />
            </Suspense>
        </div>
    );
}

fallback里写的就是组件加载时的 “Loading 占位”,比如转圈圈、提示文字,用户体验会更友好。

二、React 里的 “图片 / 内容懒加载”:react-lazyload 或 自定义组件

组件懒加载管的是 “代码包”,图片 / 内容懒加载管的是 “可视区域内才渲染内容”。我常用的有两种方案:

方案 1:直接用react-lazyload库(现成的轮子)

仓库地址: www.npmjs.com/package/rea…

下载方法:npm i react-lazyload

导入的LazyLoad就是这个库,直接包在图片外面:

import LazyLoad from 'react-lazyload';

// 在App.jsx里用:
<LazyLoad placeholder={<div>图片加载中...</div>} offset={300}>
    <img src="https://xxx.png" alt="" />
</LazyLoad>
  • placeholder:图片加载前的占位内容;
  • offset={300}提前 300px 开始加载图片(用户还没滚到,图片已经在偷偷加载了,体验更丝滑)。

完整代码(为了更好展现效果,我放了50个p标签):

import LazyLoad from 'react-lazyload';
import { lazy } from 'react'
// import Demo from './Demo'
import MyLazyLoad from './MyLazyLoad'

// 组件没有出现在可视区域时,组件代码都不会被加载,被import('./Demo')包裹的模块会单独打包
const Demo = lazy(() => import('./Demo'));

export default function App() {
    return (
        <div>
            {/* <Demo></Demo> */}
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <p>xxx</p>
            <MyLazyLoad placeholder={<div>loading...</div>} width='100px' onContentVisible={() => console.log('onContentVisible')} onClose={() => console.log('onClose')}>
                {/* <img src="https://inews.gtimg.com/om_bt/OG4Cnt2SgXAuTj-Vv77ASGszUj1BwOhUXtBCplSlBfQmAAA/641" alt="" /> */}
                <Demo></Demo>
            </MyLazyLoad>
            <LazyLoad placeholder={<div>loading...</div>} offset={300}>
                <img src="https://inews.gtimg.com/om_bt/Os3eJ8u3SgB3Kd-zrRRhgfR5hUvdwcVPKUTNO6O7sZfUwAA/641" alt="" />
            </LazyLoad>
        </div>
    )
}

可以看到最开始的界面显示的都是loading

image.png

我们慢慢往下滑:

image.png

滑到Demo照片

image.png

方案 2:自己写一个懒加载组件(比如我自己写的MyLazyLoad.jsx

我写的MyLazyLoad组件,核心就是用IntersectionObserver实现 “可视区域检测”,逻辑和之前普通网页的懒加载是通的:

// MyLazyLoad.jsx核心代码
import { useState, useRef, useEffect } from 'react';

export default function MyLazyLoad(props) {
    const { placeholder, children, offset, onContentVisible } = props;
    const [visible, setVisible] = useState(false);
    const containerRef = useRef(null);
    const elementObserver = useRef();

    // 初始化IntersectionObserver
    useEffect(() => {
        const options = {
            threshold: 0,
            rootMargin: typeof offset === 'number' ? `${offset}px` : '0px'
        };
        // 监听元素是否进入可视区域
        elementObserver.current = new IntersectionObserver(lazyLoadHandler, options);
        const node = containerRef.current;
        elementObserver.current.observe(node);

        // 卸载时停止监听
        return () => {
            elementObserver.current.unobserve(node);
        };
    }, []);

    // 元素进入可视区域后的处理
    function lazyLoadHandler(entries) {
        const [entry] = entries;
        if (entry.isIntersecting) {
            setVisible(true); // 显示真实内容
            onContentVisible?.(); // 触发“内容可见”的回调
            elementObserver.current.unobserve(containerRef.current); // 停止监听
        }
    }

    // 没进入可视区域就显示占位,进入了就显示真实内容
    return (
        <div ref={containerRef}>
            {visible ? children : placeholder}
        </div>
    );
}

用的时候和react-lazyload差不多,把要懒加载的内容包进去:

<MyLazyLoad placeholder={<div>加载中...</div>} offset={300}>
    <img src="https://xxx.png" alt="" />
    {/* 甚至可以包组件:<Demo /> */}
</MyLazyLoad>

三、“组件懒加载”+“内容懒加载”:双剑合璧

把前面的技术结合起来,就是 React 项目里的 “终极懒加载”

import { lazy, Suspense } from 'react';
import MyLazyLoad from './MyLazyLoad';

// 组件代码懒加载
const Demo = lazy(() => import('./Demo'));
export default function App() {
    return (
        <div>
            {/* 一堆内容... */}
            {/* 组件代码+组件内容 都懒加载 */}
            <Suspense fallback={<div>组件包加载中...</div>}>
                <MyLazyLoad placeholder={<div>组件内容加载中...</div>}>
                    <Demo />
                </MyLazyLoad>
            </Suspense>
        </div>
    );
}
  • 第一步:用户滑到 Demo 区域前Demo的代码包不会下载
  • 第二步:代码包下载完成后,MyLazyLoad会等 Demo 进入可视区域,再渲染 Demo 的内容;
  • 全程都有占位提示,用户不会看到 “空白” 或 “报错”

四、总结

  • React 懒加载分两类React.lazy+Suspense实现组件代码按需下载react-lazyload/ 自定义组件实现内容按需渲染
  • 核心价值:减少初始化资源开销,提升页面加载和渲染性能。

结语

普通网页的懒加载是让图片 “摸鱼”React 的懒加载则是给组件和代码都发了 “摸鱼许可证”—— 不用一开场就全员到岗,该躺平时躺平,该干活时再发力。

就好比经营一家店,不用把所有商品都堆在门口 (初始化加载所有代码),而是把暂时没人要的商品放进仓库 (单独打包组件),等顾客问到了再取出来,摆上货架(渲染内容)—— 既省了门口的空间 (页面加载性能),又不会让顾客等太久 (优化用户体验)