4、React.lazy + Suspense:React官方推荐的组件级懒加载方案
好,咱们今天聊聊 React 官方钦定的懒加载方案——React.lazy 和 Suspense。
说实话,我刚接触这个 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 实现「滚动到才加载」的效果。