目 录CONTENT

文章目录

React(十九)懒加载

React 懒加载完全指南:从原理到实战

一、什么是懒加载?

1.1 生活化类比:按需取快递

想象你点了一份外卖,里面有三道菜:一道前菜、一道主菜、一道甜点。

  • 普通加载:外卖一到,你先把三道菜全部打开摆在桌上。即使你现在只吃前菜,主菜和甜点也在桌上占地方、慢慢变凉。
  • 懒加载:你只吃前菜,主菜和甜点先放在保温袋里。等你吃完前菜,再打开主菜;吃完主菜,再打开甜点。

在网页中,懒加载就是:先加载用户当前需要的资源,其他资源等用户真正要用到的时候再加载。

1.2 React 中的官方定义

React 提供了官方 API 来实现组件懒加载:

  • React.lazy():用于动态导入组件,返回一个懒加载的组件。
  • Suspense:用于包裹懒加载组件,在组件加载完成前显示 fallback 内容(如 loading 提示、骨架屏)。
import { lazy, Suspense } from "react";

const LazyComponent = lazy(() => import("./LazyComponent"));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

1.3 懒加载的核心原理

React 懒加载的实现流程可以简化成三步:

用户访问页面
    │
    ▼
React 只渲染当前可见/需要的组件
    │
    ▼
当用户触发某行为(滚动、点击路由、打开弹窗)时
    │
    ▼
动态 import() 发起网络请求,拉取对应代码包
    │
    ▼
代码包下载完成后,Suspense 的 fallback 消失,组件渲染出来

底层依靠的是 JavaScript 的动态导入语法import()。它返回一个 Promise,只有真正执行时才会去加载对应的 JS 文件。

1.4 懒加载解决了哪些痛点?

痛点 没有懒加载 使用懒加载后
首屏加载体积过大 所有页面组件一次性打包到一个 JS 文件 只加载当前页面需要的 JS
白屏时间长 用户等待所有资源下载完成 用户先看到首屏内容,其他资源后加载
资源浪费 用户可能根本没访问的页面也下载了 访问哪个页面才下载哪个页面的代码
转化率低 加载慢导致用户流失 首屏更快,体验更好

1.5 懒加载对页面性能的实际增益

  1. 减少首屏 JS 体积:首屏只需要加载核心代码,其他代码按需拆分。
  2. 提升 LCP(Largest Contentful Paint):首屏内容更快渲染,LCP 指标更好。
  3. 节省带宽:用户没访问到的页面代码不会被下载,对移动端用户尤为重要。
  4. 提升交互响应速度:配合预加载,用户切换到下一个页面时几乎没有等待感。

二、React 懒加载的实操用法

2.1 基础用法:React.lazy + Suspense

2.1.1 安装和准备

React 16.6+ 已经内置了 React.lazySuspense,无需额外安装。

创建一个普通的组件文件:

// LazyComponent.jsx
function LazyComponent() {
  return <div>我是懒加载进来的组件!</div>;
}

export default LazyComponent;

2.1.2 使用 React.lazy 导入

// App.jsx
import { lazy, Suspense } from "react";

// 使用 React.lazy 动态导入组件
const LazyComponent = lazy(() => import("./LazyComponent"));

function App() {
  return (
    <div>
      <h1>我的页面</h1>

      {/* Suspense 包裹懒加载组件,加载期间显示 fallback */}
      <Suspense fallback={<div>正在加载组件...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

export default App;

代码拆解

代码 作用
lazy(() => import('./LazyComponent')) 声明这是一个懒加载组件,只有在渲染时才会真正加载文件。
import('./LazyComponent') 动态导入,返回一个 Promise。
Suspense 官方组件,用于包裹懒加载内容。
fallback={...} 组件加载期间显示的内容,可以是文字、loading 动画、骨架屏。

注意Suspense 必须包裹懒加载组件,否则 React 会报错。

2.2 路由懒加载:React Router v6

在多页面应用中,我们通常会按路由拆分代码包,用户进入哪个路由才加载对应的页面组件。

2.2.1 创建两个页面组件

// pages/Home.jsx
function Home() {
  return <h1>首页</h1>;
}

export default Home;
// pages/About.jsx
function About() {
  return <h1>关于我们</h1>;
}

export default About;

2.2.2 配置路由懒加载

// App.jsx
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import { lazy, Suspense } from "react";

// 路由级懒加载
const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
      </nav>

      {/* 用 Suspense 包裹整个路由区域 */}
      <Suspense fallback={<div>页面加载中...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

export default App;

关键理解

  • 用户首次访问 / 时,只加载 Home 组件的代码。
  • 用户点击“关于”切换到 /about 时,才发起 About 组件的代码请求。
  • 这样每个路由对应一个独立的 JS 文件,实现了按路由拆分代码包。

2.3 进阶用法

2.3.1 长列表滚动懒加载(可视区域加载)

场景:页面有一个长列表,下方的卡片在滚动到可视区域时才加载,避免一次性渲染过多组件。

这里使用 IntersectionObserver API 实现:

import { useEffect, useRef, useState, lazy, Suspense } from "react";

// 懒加载卡片组件
const HeavyCard = lazy(() => import("./HeavyCard"));

function LazyCard({ title }) {
  const ref = useRef(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect(); // 加载一次后不再观察
        }
      },
      { threshold: 0.1 },
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div
      ref={ref}
      style={{ height: 200, border: "1px solid #ccc", margin: 10 }}
    >
      {isVisible ? (
        <Suspense fallback={<div>卡片加载中...</div>}>
          <HeavyCard title={title} />
        </Suspense>
      ) : (
        <div>向下滚动查看内容</div>
      )}
    </div>
  );
}

export default LazyCard;
// App.jsx 中使用
import LazyCard from "./LazyCard";

function App() {
  return (
    <div>
      {Array.from({ length: 50 }).map((_, i) => (
        <LazyCard key={i} title={`卡片 ${i + 1}`} />
      ))}
    </div>
  );
}

export default App;

业务价值:长列表、瀑布流、Feed 流页面中,避免一次性渲染所有卡片,提升首屏速度和滚动流畅度。

2.3.2 弹窗组件按需加载

场景:弹窗组件代码较大,且不一定每个用户都会打开。可以在用户点击“打开弹窗”时再加载。

import { useState, lazy, Suspense } from "react";

const HeavyModal = lazy(() => import("./HeavyModal"));

function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(true)}>打开弹窗</button>

      {showModal && (
        <Suspense fallback={<div>弹窗加载中...</div>}>
          <HeavyModal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  );
}

export default App;

业务价值:表单弹窗、详情弹窗、富文本编辑器等重型组件,只在用户触发时才加载,避免拖累首屏。

2.3.3 预加载(Preload)

场景:用户鼠标悬停在按钮上时,提前加载即将访问的页面,切换到该页面时几乎无等待。

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import { lazy, Suspense } from "react";

const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));

// 预加载函数
const preloadAbout = () => {
  const AboutPromise = import("./pages/About");
  AboutPromise.then(() => console.log("About 页面已预加载"));
};

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about" onMouseEnter={preloadAbout}>
          关于
        </Link>
      </nav>

      <Suspense fallback={<div>页面加载中...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

export default App;

业务价值:适合高频跳转的页面,如电商首页到商品详情、后台管理系统的一级菜单,让交互更流畅。

2.3.4 错误边界处理

懒加载依赖网络请求,如果资源加载失败(比如服务器 404、网络断开),整个页面会崩溃。必须用错误边界兜底。

// ErrorBoundary.jsx
import { Component } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error("懒加载组件出错:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>页面加载失败</h2>
          <button onClick={() => window.location.reload()}>重新加载</button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;
// App.jsx
import { lazy, Suspense } from "react";
import ErrorBoundary from "./ErrorBoundary";

const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>加载中...</div>}>
        <Home />
      </Suspense>
    </ErrorBoundary>
  );
}

export default App;

三、开发注意事项与常见踩坑

3.1 React.lazy 只支持默认导出

问题表现

如果你用命名导出:

// MyComponent.jsx
export function MyComponent() {
  return <div>Hello</div>;
}
// App.jsx
const MyComponent = lazy(() => import("./MyComponent")); // ❌ 报错!

错误原因

React.lazy 的回调函数必须返回一个默认导出的组件。命名导出无法直接识别。

正确解法

方案一:改为默认导出

// MyComponent.jsx
export default function MyComponent() {
  return <div>Hello</div>;
}

方案二:使用包装函数转换命名导出

const MyComponent = lazy(() =>
  import("./MyComponent").then((module) => ({
    default: module.MyComponent,
  })),
);

3.2 SSR 场景不兼容 React.lazy

问题表现

在 Next.js、Remix 等 SSR 框架中直接使用 React.lazy + Suspense 会出现 hydrate 错误、服务端无法渲染等问题。

错误原因

React.lazy 依赖浏览器端的 import() 动态导入,服务端环境无法直接执行异步加载。

正确解法

使用 @loadable/component 替代。

npm install @loadable/component
import loadable from "@loadable/component";

const LoadableComponent = loadable(() => import("./HeavyComponent"), {
  fallback: <div>加载中...</div>,
});

function App() {
  return <LoadableComponent />;
}

export default App;

如果是 Next.js,更推荐使用官方内置的 dynamic

import dynamic from "next/dynamic";

const DynamicComponent = dynamic(() => import("./HeavyComponent"), {
  loading: () => <div>加载中...</div>,
});

function App() {
  return <DynamicComponent />;
}

export default App;

3.3 如何验证懒加载的代码拆分效果

方法一:浏览器 Network 面板

  1. 打开浏览器开发者工具,切换到 Network 面板。
  2. 勾选 JS 类型过滤。
  3. 首次访问页面时,观察只加载了哪些 JS 文件。
  4. 切换路由或触发懒加载时,观察是否有新的 JS 文件请求发出。

方法二:webpack-bundle-analyzer

npm install --save-dev webpack-bundle-analyzer

webpack.config.js 中配置:

const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static", // 生成静态报告文件
      openAnalyzer: true,
    }),
  ],
};

构建后会生成一个可视化报告,清楚看到每个路由/组件被拆成了哪些包。

3.4 性能优化误区

误区 1:过度拆分小组件

问题:每个组件都懒加载,会导致大量小体积的 JS 文件请求,网络往返开销反而拖慢性能。

正确做法

  • 优先拆分流页面、重型组件、不常用的弹窗。
  • 小组件、高频组件(如 Button、Input)不建议懒加载。
  • 合理设置打包工具的分包策略(如 webpack 的 splitChunks)。

误区 2:fallback 用空白占位

问题:如果 fallback 是空白,用户会以为页面卡住了。

正确做法

  • 使用 骨架屏(Skeleton Screen)模拟真实内容结构。
  • 使用带有旋转动画的 loading 提示。
  • 在路由懒加载中,fallback 可以显示页面顶部导航,减少感知等待时间。
function Skeleton() {
  return (
    <div>
      <div
        style={{
          height: 20,
          width: "80%",
          background: "#eee",
          marginBottom: 10,
        }}
      />
      <div style={{ height: 20, width: "60%", background: "#eee" }} />
    </div>
  );
}

<Suspense fallback={<Skeleton />}>
  <LazyComponent />
</Suspense>;

3.5 错误处理必须搭配错误边界

问题表现

如果懒加载文件加载失败,React 会抛出错误,导致整个应用白屏。

正确解法

参考上面 2.3.4 节的错误边界代码,始终用 ErrorBoundary 包裹 Suspense 区域。

<ErrorBoundary>
  <Suspense fallback={<Skeleton />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

四、常见疑问 Q&A

Q1:懒加载和普通导入有什么区别?

// 普通导入:构建时就把组件代码打包进主包
import Home from "./pages/Home";

// 懒加载:运行时按需加载,单独打包成一个小文件
const Home = lazy(() => import("./pages/Home"));

普通导入会在页面初始化时加载,懒加载在真正需要时才加载。

Q2:什么场景必须用懒加载?

  • 多页面应用按路由拆分。
  • 大型后台管理系统,不同功能模块代码量大。
  • 弹窗、富文本编辑器、图表、地图等重型组件。
  • 用户不一定访问到的页面(如帮助中心、设置页)。

Q3:懒加载会让页面变慢吗?

如果合理使用,不会。它减少了首屏负担,但切换页面时会有短暂的加载时间。可以配合 预加载 来消除这种延迟。

Q4:Suspense 可以包裹多个懒加载组件吗?

可以。一个 Suspense 可以包裹多个 lazy 组件,它们共享同一个 fallback

<Suspense fallback={<div>加载中...</div>}>
  <LazyA />
  <LazyB />
</Suspense>

Q5:懒加载后的文件名字是随机 hash,怎么识别?

import() 中使用魔法注释指定 chunk 名称:

const About = lazy(
  () => import(/* webpackChunkName: "about" */ "./pages/About"),
);

打包后会生成 about.xxx.js 文件,便于 Network 面板识别。

Q6:移动端适合用懒加载吗?

非常适合。移动端网络环境不稳定,懒加载能显著节省首屏流量和加载时间,提升留存率。


五、课后练习任务

练习 1:路由懒加载(基础)

目标:创建一个包含 首页关于用户中心 三个路由的 React 应用,实现路由级懒加载。

要求

  • 使用 React Router v6。
  • 每个路由组件使用 React.lazy 懒加载。
  • 配置统一的 Suspense fallback 显示“页面加载中...”。
  • 给每个路由设置 webpack chunk 名称。

练习 2:弹窗组件按需加载(进阶)

目标:实现一个按钮点击后才加载的弹窗组件。

要求

  • 弹窗组件体积较大(可以模拟一个包含大量内容的组件)。
  • 未点击按钮时,弹窗代码不加载。
  • 点击按钮后,显示 fallback,加载完成后弹窗出现。
  • 弹窗有关闭按钮,关闭后再次打开不需要重新加载(因为浏览器已缓存)。

练习 3:长列表滚动懒加载(综合)

目标:实现一个包含 100 条数据的长列表,每条数据对应一个卡片组件。

要求

  • 初始只加载首屏可见的卡片。
  • 当用户滚动到某个卡片进入可视区域时,才加载该卡片内容。
  • 使用 IntersectionObserver 监听可见性。
  • 加载过程中显示骨架屏或 loading 提示。

六、参考答案思路

练习 1 思路

  1. 创建 pages/Home.jsxpages/About.jsxpages/User.jsx 三个默认导出组件。
  2. App.jsx 中用 lazy 导入每个页面。
  3. BrowserRouter + Routes + Route 配置路由。
  4. Routes 整体包在 Suspense 中。

练习 2 思路

  1. 创建 Modal.jsx 组件。
  2. useState 控制弹窗显隐。
  3. 使用 lazy(() => import('./Modal')) 懒加载。
  4. 弹窗用 Suspense 包裹,设置 fallback
  5. 关闭弹窗时只隐藏组件,不销毁状态,让缓存生效。

练习 3 思路

  1. 创建 Card.jsx 组件,用于渲染每条数据。
  2. 创建 LazyCard 组件,内部使用 useRefIntersectionObserver 监听自身是否进入可视区域。
  3. 进入可视区域后,设置 isVisibletrue,触发懒加载组件的渲染。
  4. 列表渲染 100 个 LazyCard,每个传入不同的数据。

总结

React 懒加载是优化中大型应用性能的核心手段之一。掌握它,你需要记住四个关键点:

  1. React.lazy 用于声明懒加载组件,import() 用于动态加载文件。
  2. Suspense 用于在加载期间显示 fallback,必须有。
  3. 路由懒加载是多页应用最常用的拆分方式。
  4. 错误边界和合理的 fallback 是生产环境必不可少的兜底方案。

课后请完成三个练习,并尝试用浏览器的 Network 面板观察懒加载文件是否被正确拆分和加载。

0
博主关闭了当前页面的评论