Tree Shaking 是现代打包工具的核心优化能力,能够在构建阶段移除未被使用的代码,从而减小最终产物体积。在 React 项目中,合理利用 Tree Shaking 和配套优化手段,可以显著提升页面加载速度。
Tree Shaking 的工作原理
Tree Shaking 基于 ES Module(ESM)的静态结构特性。ESM 的 import 和 export 语法定死在编译阶段,打包工具(Webpack、Rollup、Vite 底层使用的 esbuild/Rollup)可以在不运行代码的情况下分析出模块依赖关系:
- 标记阶段:从入口文件开始,遍历所有
import语句,构建依赖图,将每个导出的变量标记为“被引用”或“未被引用”。 - 删除阶段:在生成最终 bundle 时,移除所有标记为“未被引用”的导出代码。
关键前提:代码必须使用 ES Module 语法。CommonJS(require/module.exports)是动态的,打包工具无法静态分析,Tree Shaking 将完全失效。
确保 Tree Shaking 生效的四个关键条件
1. 使用 ES Module 编写代码
// ✅ ESM:可被 Tree Shaking
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// 文件引入时只用了 add,subtract 会被移除
import { add } from './math';
// ❌ CommonJS:无法 Tree Shaking
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
2. package.json 中声明 sideEffects
sidEffects 告诉打包工具“这个包中的文件是否有副作用(即模块执行时会对外部产生影响,而不仅仅是导出值)”。如果某个模块有副作用,即使它的导出没有被引用,也不能安全删除。
// package.json
{
"name": "my-library",
"sideEffects": false // 表示整个包都无副作用,可安全 Tree Shaking
}
也可以精确指定有副作用的文件:
{
"sideEffects": ["*.css", "*.scss", "./src/some-side-effect-file.js"]
}
React 社区常见库大多已正确声明 sideEffects,但如果你自己封装组件库,必须正确配置这个字段,否则使用方在打包时无法对你的库进行 Tree Shaking。
3. 编译器保留 ESM 结构
Babel、TypeScript 等编译器可能将 ESM 转换为 CommonJS,这会破坏 Tree Shaking。需通过配置保留 ESM 结构:
Babel 配置(babel.config.js):
module.exports = {
presets: [
['@babel/preset-env', { modules: false }], // modules: false 保留 ESM
],
};
TypeScript 配置(tsconfig.json):
{
"compilerOptions": {
"module": "ESNext", // 使用 ES Module
"moduleResolution": "bundler" // 现代打包工具推荐
}
}
4. 使用生产模式构建
开发模式下打包工具通常不会执行 Tree Shaking,必须使用 production 模式:
# Vite
vite build
# Webpack
webpack --mode production
Tree Shaking 对具体库的影响
Lodash
直接引入整个 Lodash 会导入所有函数,导致 bundle 激增:
// ❌ 导入整个 Lodash:约 70KB(gzipped)
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ 按需引入:仅 2KB
import debounce from 'lodash/debounce';
debounce(fn, 300);
更推荐使用 lodash-es(ES Module 版本)配合 Tree Shaking:
import { debounce } from 'lodash-es';
组件库(Ant Design、Material UI)
现代组件库通常支持按需导入,例如 Ant Design 5.x 已原生支持 Tree Shaking:
// ✅ Tree Shaking 后只保留 Button 相关代码
import { Button, Input } from 'antd';
对于老旧版本(如 Ant Design 4.x 及之前),需要使用 babel-plugin-import 或手动路径引入:
import Button from 'antd/lib/button';
import 'antd/lib/button/style';
打包体积分析与优化工具
1. 使用 bundle 分析工具
在优化之前,先了解“什么东西占了体积”:
Rollup/Vite 生态可使用 rollup-plugin-visualizer:
npm install -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({ open: true, gzipSize: true, brotliSize: true }),
],
};
运行 vite build 后会自动打开一个可视化页面,你可以直观看到各模块的体积占比,快速定位体积异常的依赖。
2. 针对性优化策略
- moment.js → dayjs:moment.js 约 230KB(未压缩),dayjs 仅 2KB,API 几乎兼容。或使用
date-fns按需引入。 - 多语言文件排除:moment.js、highlight.js 等库的 locale 文件体积巨大,可通过 Webpack 插件或配置忽略。
- 重复依赖去重:分析报告中出现同一库的不同版本时,通过
package.json的resolutions或overrides强制统一。
按需加载与代码分割
Tree Shaking 移除的是模块内部的死代码,而代码分割(Code Splitting)则是将代码拆分成多个文件,按需加载。两者互补。
1. 路由级懒加载
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
每个路由对应一个独立的 chunk 文件,用户访问时才加载相关代码。
2. 组件级懒加载
对体积较大、不常用的组件进行异步加载:
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Report() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>查看图表</button>
{showChart && (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
3. 公共模块自动分包
现代打包工具可自动提取公共依赖。例如 Vite 中配置:
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'], // 框架单独分包
common: ['lodash-es', 'axios'], // 常用库分包
},
},
},
},
};
这样,框架代码和常用工具库会单独生成文件,利用浏览器缓存减少重复下载。
完整优化链路
从源码到最终产物的体积控制是一个系统工程:
- 源头控制:优先选择天然支持 Tree Shaking 的库(ESM 版本),避免使用体量过大的工具库。
- 配置保障:确保
sideEffects正确声明、编译器保留 ESM 结构、使用生产模式构建。 - 按需拆分:路由懒加载 + 组件懒加载 + 公共依赖分包,让首屏只加载必要代码。
- 分析与迭代:通过可视化工具定期分析 bundle 构成,定位体积大户,持续优化。
这套方法在实际项目中通常能减少 40%–60% 的初始加载体积,对用户侧的性能体验提升立竿见影。