4、React.lazy + Suspense:React官方推荐的组件级懒加载方案

好,咱们今天聊聊 React 官方钦定的懒加载方案——React.lazySuspense

说实话,我刚接触这个 API 的时候,第一反应是:这不就是动态 import 的语法糖吗?后来在项目里用了几次,才发现事情没那么简单。它背后其实是一整套组件加载状态管理的思路。

4.1 为什么需要 React.lazy?

你想想看,以前我们做代码分割,要么用 require.ensure,要么自己封装一个高阶组件。每次都要手动处理 loading、error 这些状态,写起来挺烦的。

React.lazy 的出现,说白了就是帮你把「异步加载组件」这件事标准化了。它接收一个返回 Promise 的函数,这个 Promise 最终要 resolve 成一个模块的默认导出。

核心用法:

const LazyComponent = React.lazy(() => import('./HeavyComponent'));

就这么一行,React 就知道这个组件需要异步加载。我在项目中遇到过不少团队,把整个路由级别的页面都包了一层 lazy,结果首屏加载确实快了不少。

4.2 Suspense:加载状态的优雅处理

光有 lazy 还不够。组件加载需要时间,这期间页面显示什么?空白?转圈?还是骨架屏?

这就是 Suspense 的用武之地了。

import React, { Suspense } from 'react';

const LazyDashboard = React.lazy(() => import('./Dashboard'));

function App() {
  return (
    <div>
      <h1>我的应用</h1>
      <Suspense fallback={<div>加载中...</div>}>
        <LazyDashboard />
      </Suspense>
    </div>
  );
}

嗯,这里要注意:fallback 可以传任何 React 节点。我个人习惯用骨架屏组件,用户体验比单纯的 spinning 好很多。

4.3 配合路由做懒加载

实际项目中,最常用的场景其实是路由级别的懒加载。每个页面一个 chunk,用户访问哪个页面就加载哪个。

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import React, { Suspense } from 'react';

const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const Contact = React.lazy(() => import('./pages/Contact'));

function AppRouter() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageSkeleton />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

我曾经在一个后台管理系统里这么搞,首屏 JS 从 2.3MB 降到了 400KB。用户打开首页的速度快了将近一倍。

4.4 多个懒加载组件共用一个 Suspense

有个小技巧你可能不知道:多个 lazy 组件可以放在同一个 Suspense 里。它们会共享同一个 fallback。

<Suspense fallback={<FullPageLoader />}>
  <LazyHeader />
  <LazySidebar />
  <LazyMainContent />
</Suspense>

这样做的好处是,只要有一个组件还在加载,整个区域就显示 loading。等所有组件都加载完了,再一次性渲染出来。避免了页面东一块西一块地闪烁。

4.5 错误边界:处理加载失败

网络不是永远可靠的。用户网络差的时候,懒加载可能会失败。这时候怎么办?

React 官方推荐的做法是配合 Error Boundary 使用。

class ErrorBoundary extends React.Component {
  state = { hasError: false };

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

  render() {
    if (this.state.hasError) {
      return <h2>页面加载失败,请刷新重试</h2>;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary>
  <Suspense fallback={<Loader />}>
    <LazyPage />
  </Suspense>
</ErrorBoundary>

注意:Error Boundary 目前只能用 class 组件实现。Hooks 版本还在实验阶段。我曾经踩过这个坑,以为函数组件也能用,结果折腾了半天。

4.6 React.lazy 的限制

任何技术都有它的边界。React.lazy 目前只支持默认导出(default export)。如果你用的是命名导出,得先转一层。

// 不行的写法
const LazyComp = React.lazy(() => import('./module').then(m => m.NamedExport));

// 正确的做法
// 在 module 里加一个默认导出
export { NamedExport };
export default NamedExport;

另外,lazy 不能用在服务端渲染(SSR)中。如果你用 Next.js 或者 Remix,它们有自己的动态加载方案。

4.7 性能优化的最佳实践

最后分享几个我在实战中总结的经验:

场景 建议 原因
首屏组件 不要 lazy 首屏需要立即渲染,懒加载反而增加延迟
弹窗/抽屉 推荐 lazy 用户不一定触发,按需加载最合适
路由页面 推荐 lazy 每个页面独立 chunk,按路由加载
第三方库 配合 lazy 比如图表库、富文本编辑器,体积大且不常用

小提示:可以用 React.lazy 配合 webpack 的魔法注释来命名 chunk,方便调试。

const LazyChart = React.lazy(() => import(/* webpackChunkName: "chart" */ './Chart'));

好了,React.lazy + Suspense 这套方案,说白了就是帮你把「异步加载组件」这件事做得更优雅。它不复杂,但用好了确实能显著提升用户体验。下一节我们聊聊更高级的玩法——如何结合 Intersection Observer 实现「滚动到才加载」的效果。