构建与分包
通过代码分割与构建配置减小首屏 JavaScript 体积。运行时优化见 runtime。
核心概念
「构建优化」和「运行时优化」解决的是不同阶段的问题:构建优化关心用户第一次打开页面要下载多少 JS/CSS、多久能看到内容;运行时优化关心页面已经加载完成后,交互是否流畅。两者常被混为一谈,但优化手段完全不同——构建侧的核心武器是代码分割:把不是「首屏必须」的代码拆成独立文件,延迟到真正需要时才加载。
React.lazy 与 Suspense
import { lazy, Suspense } from "react";
const Chart = lazy(() => import("./Chart"));
function Page() {
return (
<Suspense fallback={<p>图表加载中…</p>}>
<Chart />
</Suspense>
);
}
import() 由打包工具(Vite、Webpack)拆成独立 chunk,访问路由或条件满足时再加载。
路由级分割
在 React Router 配置中对页面组件使用 lazy,是 SPA 最常见的分割方式:
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/dashboard",
lazy: async () => {
const { DashboardPage } = await import("./pages/DashboardPage");
return { Component: DashboardPage };
},
},
]);
每个路由对应的页面代码只在用户访问该路由时才下载,首屏只需加载入口路由的代码。
组件级分割:按交互延迟加载
// 富文本编辑器体积通常很大,仅在用户点击"编辑"后才需要
const RichTextEditor = lazy(() => import("./RichTextEditor"));
function ArticleDetail() {
const [editing, setEditing] = useState(false);
return (
<div>
<button onClick={() => setEditing(true)}>编辑</button>
{editing && (
<Suspense fallback={<Spinner />}>
<RichTextEditor />
</Suspense>
)}
</div>
);
}
对话框、图表、富文本编辑器、地图等「体积大但不总是需要」的组件,都适合用这种「按需触发」的懒加载模式,而不仅限于路由级别。
Tree Shaking
使用 ESM 导入,生产构建开启压缩;避免 import entire library:
// 较好:按需
import { debounce } from "lodash-es";
// 注意:部分库需查文档是否支持 tree-shaking
// 反例:整包导入,即使只用一个函数也会打入全部代码(若库不支持良好的 tree-shaking)
import _ from "lodash";
_.debounce(fn, 300);
// 更好:明确从 ESM 版本按需导入
import debounce from "lodash-es/debounce";
判断一个库是否支持良好的 tree-shaking,可查看其 package.json 是否有 "sideEffects": false 声明,以及是否提供 ESM 格式的产物(module 字段)。
Vite 生产构建
// vite.config.ts 片段
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"],
},
},
},
},
});
manualChunks 将 React 等稳定依赖单独打包,利于长期缓存(具体策略按项目体积分析调整)。
分析构建产物
pnpm add -D rollup-plugin-visualizer
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [react(), visualizer({ open: true, gzipSize: true })],
});
构建后自动打开一份可视化的 bundle 分析报告,用矩形面积直观展示各模块占比,快速定位「哪个依赖占用了大量体积」。常见发现包括:误打入整个 UI 库、日期库(如 moment 完整语言包)、重复打包的多份相同依赖。
图片与静态资源优化
// Vite 支持在导入时进行部分转换/内联小文件
import logo from "./logo.svg?url";
import iconRaw from "./icon.svg?raw";
大图片建议转 WebP/AVIF 并结合 CDN 处理裁剪与压缩,而不是把原图直接打进构建产物;小于阈值(默认 4KB)的资源 Vite 会自动 base64 内联,减少请求数但增加 JS/CSS 体积,需要权衡。
最佳实践
- 路由级懒加载是「投入产出比最高」的第一步优化,几乎所有 SPA 都该做。
- 大体积、非首屏必需的组件(富文本、图表、地图)按交互时机懒加载,不要一次性打进首屏 bundle。
- 定期跑一次 bundle 分析(可作为 CI 步骤,体积超过阈值时告警),而不是等用户反馈"加载慢"才排查。
manualChunks只对变化频率低的依赖(React、UI 库)有意义,业务代码频繁改动,拆多了反而增加请求数。- 优先选择原生支持 ESM/tree-shaking 的库(
date-fns优于moment,lodash-es优于lodash)。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 首屏 JS 体积异常大 | 未做路由级分割,所有页面代码打进一个 bundle | 对页面组件使用 lazy + Suspense |
| bundle 分析发现某个库占比异常高 | 整包导入了只用到一小部分功能的库 | 改为按需导入,或换更轻量的替代库 |
manualChunks 配置后加载变慢 | 拆分粒度过细,导致请求数暴增 | 收敛拆分策略,只拆真正稳定且体积大的依赖 |
| 图片资源体积过大拖慢首屏 | 直接打包原始大图,未压缩/转格式 | 使用 WebP/AVIF、CDN 处理、按需加载 |
| Suspense fallback 频繁闪烁 | 懒加载的 chunk 体积小但网络延迟高,加载时间短却仍触发 fallback | 评估是否需要懒加载该组件,或添加最短展示时长 |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| React:lazy | 懒加载 |
| React:Suspense | 边界 |
| Vite:构建生产版本 | 生产构建 |
| rollup-plugin-visualizer | 构建体积分析 |
| web.dev:Code splitting | 代码分割实践 |
相关文章
运行时优化
减少不必要的组件重渲染与昂贵计算。涵盖重渲染成因、memo/useMemo/useCallback、虚拟滚动、Web Worker、Profiler 分析、最佳实践与常见坑。构建侧见 build。
编译与打包优化
构建阶段的代码分割、Tree Shaking 与 SFC 编译优化。运行时见 runtime。
渲染与协调
React 将组件渲染为虚拟 DOM 树,再协调(reconciliation)到浏览器 DOM。理解该过程有助于解释 key、memo 与性能优化。涵盖渲染流程、Fiber、startTransition、Suspense、最佳实践与常见坑。
工程化
新建 React 项目时,官方与社区主流工具为 Vite;全栈场景常用 Next.js。涵盖 Vite + React、Create React App 现状、Next.js、目录结构、环境变量、部署、最佳实践与常见坑。
TypeScript
React 官方推荐 TypeScript。类型主要落在 Props、事件、Ref 与 Hooks 返回值上。见 组件。
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
Series
performance
1 / 2