代码分割
默认情况下,打包工具会把你的整个应用打包成一个 JavaScript 文件,文件顶部的每一个 import 都会被加入其中。这个单一的包会随着应用增长而变大,浏览器必须下载完整个包才能开始渲染任何内容。
在性能好、网络快的设备上你可能察觉不到。但在网络差的手机上,一个庞大的库文件就能卡住整个首次加载,包括那些根本不需要这个库的功能。这门课程用一个依赖了大型模拟数据包的商品列表来演示这个问题:如果所有代码都打包在一起,即使是同一个页面上的计数器在限速网络下也要等超过二十秒才能出现。
代码分割 就是解决方案。它把包分成若干块(称为 chunk),只在用户真正需要的时候才下载这一块。用户永远不会用到的功能根本不用下载。
动态 import()
这个机制的基础是普通的 JavaScript。文件顶部的 import 总是会运行。但你也可以把 import() 当作函数来调用,这样就能在代码的任何地方按需加载一个模块,它会返回一个 Promise,resolve 时提供该模块。
// 总是被加载,属于主包的一部分
import { formatPrice } from './format'
// 只在这个处理器运行时才被加载
async function showChart() {
const { renderChart } = await import('./charts')
renderChart()
}打包工具会把每一个 import() 调用视为一条分割线:被 import 的模块,加上它独有的依赖(不与主包共享的部分),会被放进一个独立的 chunk 中,在调用执行的第一时刻通过网络获取。
lazy 和 Suspense
React 用一对工具包装了这个机制。lazy 接收一个返回动态 import() 的函数,给你返回一个可以像其他组件一样渲染的组件。这个加载器的 Promise 必须 resolve 到一个模块,其中 默认导出 就是该组件;模块中的其他命名导出没问题。对于你无法控制的、只导出命名组件的文件,可以改造 Promise:lazy(() => import('./charts').then(m => ({ default: m.RevenueChart })))。
渲染一个 lazy 组件会触发 suspense:React 暂停那部分树的 渲染,直到 chunk 到达。光有暂停是错误,所以你需要用一个 Suspense 边界包裹 lazy 组件,并提供 fallback prop,即下载过程中显示的 UI。
import { lazy, Suspense, useState } from 'react'
const ProductsList = lazy(() => import('./ProductsList'))
export default function App() {
const [showProducts, setShowProducts] = useState(false)
return (
<>
<Counter />
<button onClick={() => setShowProducts(prev => !prev)}>
Show products
</button>
{showProducts && (
<Suspense fallback={<h2>Loading...</h2>}>
<ProductsList />
</Suspense>
)}
</>
)
}现在主包不再包含 ProductsList 及其庞大的依赖。计数器立刻出现。点击按钮会开始下载 chunk,fallback 在加载时显示,组件在到达时渲染。第一次加载后模块会被缓存,再次隐藏和显示就是瞬间的事。
版本说明
lazy 和 Suspense 是从 react 导入的。较老的代码,包括这门课程的课时内容,写成 React.lazy 和 React.Suspense;它们是同样的 API。
从路由开始分割
你可以去找单个庞大的组件,但大多数应用的默认分割线是路由。每个页面是一个自然的单元:访问主页的用户根本不需要仪表盘的代码,所以让每个页面都成为一个 lazy 组件,由 路由 来决定哪些 chunk 才需要下载。
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home'))
const Vans = lazy(() => import('./pages/Vans'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
export default function App() {
return (
<BrowserRouter>
<Suspense fallback={<h2>Loading...</h2>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/vans" element={<Vans />} />
<Route path="/host" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}一个围绕路由的 Suspense 边界覆盖了每个页面。首次加载会获取主包加上当前 URL 对应的 chunk;其他页面的代码在用户导航时才下载。这种宽泛的范围有代价:导航到一个还没有到达的 chunk 时,整个路由区域会被替换成 fallback,所以要把共享的布局(如头部和导航)放在边界外,这样在切换时它们能保持不变。
分割有下限。每个 chunk 都要多付出一次网络请求和一段 fallback UI 的时间,所以分割一个微小的组件得不偿失。小组件和用户立刻能看到的首屏内容应该留在主包中。分割那些很重、很少用或被导航隐藏的东西。
你用 lazy 包裹一个动态 import,把组件放在带有 fallback(比如加载消息)的 Suspense 中,React 就会在代码下载时显示 fallback。你应用的其余部分立刻就会出现。
接下来:身份验证,会讲会话、令牌和服务端验证如何结合在一起。

