人人都会AI编程

Tree Shaking、打包体积优化

更新时间:2026-07-10

Tree Shaking 是现代打包工具的核心优化能力,能够在构建阶段移除未被使用的代码,从而减小最终产物体积。在 React 项目中,合理利用 Tree Shaking 和配套优化手段,可以显著提升页面加载速度。

Tree Shaking 的工作原理

Tree Shaking 基于 ES Module(ESM)的静态结构特性。ESM 的 importexport 语法定死在编译阶段,打包工具(Webpack、Rollup、Vite 底层使用的 esbuild/Rollup)可以在不运行代码的情况下分析出模块依赖关系:

  1. 标记阶段:从入口文件开始,遍历所有 import 语句,构建依赖图,将每个导出的变量标记为“被引用”或“未被引用”。
  2. 删除阶段:在生成最终 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.jsonresolutionsoverrides 强制统一。

按需加载与代码分割

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'],        // 常用库分包
        },
      },
    },
  },
};

这样,框架代码和常用工具库会单独生成文件,利用浏览器缓存减少重复下载。

完整优化链路

从源码到最终产物的体积控制是一个系统工程:

  1. 源头控制:优先选择天然支持 Tree Shaking 的库(ESM 版本),避免使用体量过大的工具库。
  2. 配置保障:确保 sideEffects 正确声明、编译器保留 ESM 结构、使用生产模式构建。
  3. 按需拆分:路由懒加载 + 组件懒加载 + 公共依赖分包,让首屏只加载必要代码。
  4. 分析与迭代:通过可视化工具定期分析 bundle 构成,定位体积大户,持续优化。

这套方法在实际项目中通常能减少 40%–60% 的初始加载体积,对用户侧的性能体验提升立竿见影。