代码分割
本教程共 50 篇 · 第 42 篇 · 更新于 2026-07-29 · 约 6 分钟阅读
42. 代码分割
本节目标:学会用 React.lazy 把组件拆成按需加载的 chunk,减少首屏下载体积,提升页面加载速度。
为什么要代码分割
默认情况下,Vite 会把所有组件打包成一个 index.js。组件少的时候没问题,一旦上了规模——图表库、富文本编辑器、地图组件——这个文件就膨胀了。
用户打开首页,得下载整个应用的 JS,包括他可能永远不会看的”设置页面”和”数据报表”。浪费带宽,首屏还慢。
代码分割的思路:只下载当前页面需要的代码,其他部分等用到时再下载。
React.lazy:声明式懒加载
React.lazy 接受一个返回动态 import() 的函数,返回一个可以正常渲染的组件:
import { lazy, Suspense } from 'react';
// 静态 import:打包进主文件
// import Chart from './Chart.js';
// 动态 import:拆成单独文件,按需加载
const Chart = lazy(() => import('./Chart.js'));
function Dashboard() {
return (
<Suspense fallback={<div>图表加载中……</div>}>
<Chart />
</Suspense>
);
}
Vite 看到 import('./Chart.js') 会自动把 Chart 拆成单独的 JS 文件。只有 Chart 第一次被渲染时才会下载。
Note
lazy的import()返回的模块必须有default导出。所以被懒加载的组件要用export default function Chart()这种写法。
按路由分割
最常见的用法是按路由分割代码:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home.js'));
const Dashboard = lazy(() => import('./pages/Dashboard.js'));
const Settings = lazy(() => import('./pages/Settings.js'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<p>页面加载中……</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
用户访问 / 只下载 Home 的代码。点进 /dashboard 才下载 Dashboard 的代码。每个路由都是独立的 chunk。
条件加载
不只是路由,任何”不立即需要”的组件都可以懒加载:
import { lazy, Suspense, useState } from 'react';
const HeavyModal = lazy(() => import('./HeavyModal.js'));
function Page() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>打开弹窗</button>
{showModal && (
<Suspense fallback={null}>
<HeavyModal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
);
}
弹窗的 JS 只有用户点击”打开”时才下载。不点就不下载。
预加载:提前下载
懒加载有个问题——用户点击后要等代码下载,体验不好。解决办法是预加载:在用户可能操作之前就开始下载。
const Chart = lazy(() => import('./Chart.js'));
function Dashboard() {
return (
<div>
<button
onMouseEnter={() => import('./Chart.js')} // 鼠标悬停时开始下载
onClick={() => setShowChart(true)}
>
显示图表
</button>
{showChart && (
<Suspense fallback={<div>加载中……</div>}>
<Chart />
</Suspense>
)}
</div>
);
}
鼠标悬停到按钮上时就开始下载 Chart 的代码。等用户真正点击下载早就完成了,零等待。
也可以用 useEffect 在空闲时预加载:
useEffect(() => {
// 页面加载完成后,空闲时预加载
requestIdleCallback(() => import('./Chart.js'));
}, []);
Tip预加载不是银弹。别一次性预加载太多文件,否则和没分割一样。只预加载用户最可能访问的 1-2 个模块。
命名 chunk
默认情况下,Vite 给 chunk 起数字名 0.js、1.js。调试时看不出哪个是哪个。可以用魔法注释命名:
const Chart = lazy(() => import(/* webpackChunkName: "chart" */ './Chart.js'));
Vite 也支持这个注释,会生成 chart.js 而不是 0.js。
常见坑
lazy 不能在组件内部声明
如 §37 所述,lazy 必须在模块顶层声明。在组件内部声明会导致每次渲染创建新的组件类型,所有 state 都丢了。
lazy 组件必须用 Suspense 包裹
// ❌ 错误:没有 Suspense,lazy 组件会报错
function App() {
return <Chart />;
}
// ✅ 正确
function App() {
return (
<Suspense fallback={<div>加载中</div>}>
<Chart />
</Suspense>
);
}
Chart 的代码还没下载完时,渲染会”挂起”(suspend)。没有 Suspense 边界捕获这个挂起,React 就会报错。
服务端渲染的兼容
React.lazy 不支持服务端渲染(SSR)。如果你的应用用了 SSR(比如 Next.js),需要用 @loadable/component 这类库替代。
分割策略
不是所有组件都该懒加载。分割太细反而不好——每个 chunk 都有请求开销。
好的分割策略:
- 按路由分割:每个路由页面一个 chunk,最基本也最有效
- 按功能分割:图表、编辑器、地图这些重组件单独分割
- 按条件分割:弹窗、折叠面板、Tab 内容这些不立即显示的分割
不该分割的:
- 首屏必须显示的组件
- 很小的组件(几十字节,分割了反而多一个请求)
- 频繁切换的组件(每次切换都重新下载体验差)
Tip用
vite-bundle-visualizer看看你的打包结果,找出最大的 chunk,针对性分割。