Webpack Tree Shaking 面试题整理

1. 什么是 Tree Shaking?解决什么问题?

核心思路(一句话)

Tree Shaking 就是在构建阶段分析模块之间的依赖关系和导出使用情况,把确定没有被使用、且可以安全删除的代码从最终产物中移除,从而减小打包体积。

解决方案流程图

text 复制代码
源代码
  │
  ▼
ES Module 静态分析
  │
  ├── 分析 import / export
  │
  ▼
建立模块依赖图
  │
  ▼
判断哪些导出真正被使用
  │
  ▼
标记未使用代码
  │
  ▼
代码生成 / 压缩
  │
  ▼
删除无用代码
  │
  ▼
更小的最终 Bundle

结构化逻辑

主要矛盾:

最终 Bundle 中包含了运行时根本不会执行的代码,导致 JavaScript 体积变大。

次要矛盾:

Webpack 必须能够静态判断"哪些代码可以安全删除",否则贸然删除可能改变程序行为。

所以 Tree Shaking 的核心不是简单的:

"发现没调用的函数 → 删除。"

而是:

静态分析模块依赖和导出使用关系 → 标记未使用代码 → 在满足安全条件的情况下删除。


2. 举例说明 Tree Shaking 是怎么工作的?

例如:

js 复制代码
// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

入口文件:

js 复制代码
// main.js
import { add } from './math.js';

console.log(add(1, 2));

从依赖关系看:

text 复制代码
main.js
  │
  │ import { add }
  ▼
math.js
  ├── add        ← 被使用
  └── subtract   ← 没有被使用

理论上的 Tree Shaking 结果:

js 复制代码
function add(a, b) {
  return a + b;
}

console.log(add(1, 2));

subtract 就没有必要进入最终产物。

但是要注意

Tree Shaking 并不是看到"函数没有调用"就一定删除。

例如:

js 复制代码
export function foo() {
  console.log('foo');
}

foo();

或者:

js 复制代码
export function foo() {
  console.log('foo');
}

foo();

函数本身虽然没有被其他模块导入,但模块内部可能执行了它。

更典型的是:

js 复制代码
// init.js
console.log('初始化程序');

export const value = 100;

即使:

js 复制代码
import './init.js';

没有使用任何导出,console.log('初始化程序') 仍然可能必须保留。

因此:

Tree Shaking 的核心难点不是"找没用的代码",而是判断删除代码是否会改变程序语义。


3. Webpack 如何开启 Tree Shaking?

核心思路(一句话)

Webpack 通过 optimization.usedExports 标记模块中哪些导出没有被使用,再结合压缩器真正删除这些无用代码。

典型配置:

js 复制代码
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production',

  optimization: {
    // 分析每个模块的导出哪些被使用、哪些没有被使用
    usedExports: true,

    // 对最终代码进行压缩。
    // 压缩器会根据 Webpack 的标记结果删除
    // 可以安全删除的无用代码。
    minimize: true,

    minimizer: [
      new TerserPlugin(),
    ],
  },
};

usedExports 负责分析并标记哪些 export 被使用;它本身主要负责"标记",真正把代码从产物中删除通常还需要后续的代码压缩阶段。

可以理解为:

text 复制代码
usedExports
    ↓
告诉 Webpack:
"这个导出没人用"
    ↓
标记未使用代码
    ↓
Terser 等压缩器
    ↓
真正删除无用代码

4. 为什么 Tree Shaking 通常要求使用 ES Module?

核心思路(一句话)

因为 ES Module 的 import / export 具有静态结构,Webpack 可以在运行前分析依赖关系;CommonJS 的 require 可以动态执行,静态分析难度更高。

例如:

js 复制代码
import { add } from './math.js';

Webpack 在构建阶段就能知道:

text 复制代码
main.js
  ↓
需要 math.js 的 add
  ↓
subtract 没有被使用

而 CommonJS:

js 复制代码
const name = getModuleName();

const math = require(name);

运行之前甚至无法确定到底加载哪个模块:

text 复制代码
require(name)
    │
    ▼
name 运行时才能确定
    │
    ▼
静态分析困难

因此:

text 复制代码
ES Module
静态 import/export
        ↓
构建阶段可分析
        ↓
适合 Tree Shaking

而:

text 复制代码
CommonJS
require() 可以动态执行
        ↓
依赖关系可能运行时才能确定
        ↓
Tree Shaking 难度更高

但不要绝对化

不要在面试中说:

"CommonJS 完全不能 Tree Shaking。"

更准确的说法是:

Tree Shaking 最适合具有静态依赖关系的 ES Module。Webpack 对 CommonJS 也存在一定的静态分析能力,但动态 require 等场景会限制 Tree Shaking 效果。


5. Tree Shaking 和 sideEffects 是什么关系?

这是面试中非常容易继续追问的问题。

核心思路

usedExports 解决"哪些导出没有被使用",sideEffects 解决"整个模块在没人使用其导出时能不能直接跳过"。

例如:

js 复制代码
// polyfill.js
Array.prototype.myMethod = function () {
  // ...
};

然后:

js 复制代码
import './polyfill.js';

虽然没有使用任何 export:

text 复制代码
polyfill.js
    ↓
没有 export 被使用

但这个模块执行时会修改全局对象:

text 复制代码
polyfill.js
    ↓
修改 Array.prototype
    ↓
产生副作用

所以不能简单删除。


sideEffects 配置

例如:

json 复制代码
{
  "sideEffects": false
}

表示:

这个包中的模块被认为没有副作用,如果某个模块的导出没有被使用,可以更积极地进行删除。

例如:

json 复制代码
{
  "sideEffects": [
    "*.css",
    "./src/polyfill.js"
  ]
}

意思是:

text 复制代码
默认认为模块没有副作用
        │
        ├── *.css
        │     ↓
        │   有副作用
        │
        └── polyfill.js
              ↓
            有副作用

这样可以避免误删 CSS、Polyfill 等必须执行的模块。


6. usedExports 和 sideEffects 有什么区别?

这是一个很好的面试对比题。

配置 核心作用
usedExports 分析某个模块的哪些导出被使用
sideEffects 判断模块整体是否可以在没有使用导出的情况下被跳过
minimize 对生成代码进行压缩,实际删除大量无用代码
TerserPlugin Webpack 常见的 JavaScript 压缩实现

可以用一句话记:

text 复制代码
usedExports
→ 哪个 export 没用?

sideEffects
→ 整个模块能不能不执行?

Terser
→ 这些已经确定没用的代码能不能删掉?

7. Tree Shaking 的底层实现原理是什么?

核心思路(一句话)

Webpack 先基于 ES Module 的静态结构建立依赖图并分析 export 的使用情况,再标记未使用代码,最后通过压缩阶段进行删除。

架构图

text 复制代码
                  源代码
                    │
                    ▼
              Module Parser
                    │
                    ▼
          ┌──────────────────┐
          │   模块依赖关系图   │
          └──────────────────┘
                    │
                    ▼
       分析 import / export 使用关系
                    │
                    ▼
             usedExports
                    │
          ┌─────────┴─────────┐
          │                   │
       被使用              未被使用
          │                   │
          ▼                   ▼
        保留              标记删除
                              │
                              ▼
                         代码压缩器
                              │
                              ▼
                       删除 Dead Code
                              │
                              ▼
                         最终 Bundle

为什么叫 Tree Shaking?

因为模块依赖关系可以抽象成一棵树:

text 复制代码
入口模块
   │
   ├── A
   │    ├── A1
   │    └── A2
   │
   └── B
        ├── B1
        └── B2

如果:

text 复制代码
入口
 └── A
      └── A1

真正使用到的只有:

text 复制代码
A
└── A1

那么:

text 复制代码
A2
B
├── B1
└── B2

如果确定没有副作用,就可以从最终产物中消除。


8. Tree Shaking 的边界场景有哪些?

场景一:动态模块加载

js 复制代码
const moduleName = getModuleName();

const module = require(moduleName);

依赖关系运行时才确定:

text 复制代码
构建阶段
   ↓
无法确定 moduleName
   ↓
无法完整确定依赖关系
   ↓
Tree Shaking 能力受限

场景二:模块存在副作用

js 复制代码
// init.js
window.appInitialized = true;

即使:

js 复制代码
import './init.js';

没有使用 export,也不能随意删除:

text 复制代码
删除 init.js
    ↓
window.appInitialized 不再设置
    ↓
程序行为发生变化

场景三:CSS

例如:

js 复制代码
import './style.css';

CSS 本身没有 JavaScript export,但导入行为会产生实际效果。

因此很多项目会:

json 复制代码
{
  "sideEffects": [
    "*.css"
  ]
}

场景四:CommonJS

js 复制代码
const utils = require('./utils');

尤其是动态:

js 复制代码
require(`./${name}.js`);

静态分析能力明显受限。


9. Tree Shaking 和代码分割有什么区别?

这个也很容易混淆。

Tree Shaking

解决:

"不要的代码删掉。"

text 复制代码
一个 Bundle

┌──────────────────────┐
│ 使用代码             │
│ 无用代码 ← 删除       │
└──────────────────────┘

Code Splitting

解决:

"代码不要全部放在一个 Bundle 中,拆成多个 Chunk,按需加载。"

text 复制代码
App
 │
 ├── main.js
 │
 ├── dashboard.js
 │
 └── editor.js

例如:

js 复制代码
const Editor = await import('./Editor.js');

形成:

text 复制代码
首屏
 ↓
main.js

用户打开编辑器
 ↓
Editor.js
 ↓
动态加载

所以:

text 复制代码
Tree Shaking
→ 减少"代码总量"

Code Splitting
→ 减少"单次加载量"

两者可以一起使用。


10. Tree Shaking 的实际使用场景

最典型的是工具库。

例如:

js 复制代码
import { debounce } from 'some-utils';

如果库本身使用 ES Module:

text 复制代码
some-utils
├── debounce
├── throttle
├── cloneDeep
├── merge
├── ...

而项目只使用:

text 复制代码
debounce

理想情况下:

text 复制代码
debounce       ← 保留
throttle       ← 删除
cloneDeep      ← 删除
merge          ← 删除

这样可以减少 Bundle 体积。


11. Tree Shaking 最佳实践

① 优先使用 ES Module

js 复制代码
import { debounce } from './utils.js';

而不是大量使用动态:

js 复制代码
require(moduleName);

② 正确声明 sideEffects

如果你的包确实没有副作用:

json 复制代码
{
  "sideEffects": false
}

如果存在 CSS、Polyfill 等副作用:

json 复制代码
{
  "sideEffects": [
    "*.css",
    "./src/polyfill.js"
  ]
}

不要为了 Tree Shaking 效果随便写 sideEffects: false。

否则可能出现:

text 复制代码
错误声明
   ↓
Webpack 认为模块无副作用
   ↓
模块被删除
   ↓
初始化代码 / CSS / Polyfill 不执行
   ↓
程序出现异常

③ 生产环境构建

通常:

js 复制代码
module.exports = {
  mode: 'production'
};

Webpack 会启用生产环境相关优化,包括代码压缩等。

因此实际项目一般不需要手工把所有优化开关逐个配置。


12. 一个完整的 Tree Shaking 示例

项目结构:

text 复制代码
src/
├── main.js
└── math.js

math.js

js 复制代码
// math.js

// 这个函数会被 main.js 使用。
// 因此它应该被保留在最终产物中。
export function add(a, b) {
  return a + b;
}

// 这个函数没有被 main.js 使用。
// 在满足 Tree Shaking 条件的情况下,最终产物中可以被删除。
export function subtract(a, b) {
  return a - b;
}

// 同样没有被使用,也可以被删除。
export function multiply(a, b) {
  return a * b;
}

main.js

js 复制代码
// main.js

// 只导入 add。
// Webpack 可以通过静态分析知道:
// math.js 中只有 add 是当前模块需要使用的导出。
import { add } from './math.js';

// 使用 add。
console.log(add(10, 20));

webpack.config.js

js 复制代码
const path = require('path');

module.exports = {
  // Webpack 的入口文件。
  entry: './src/main.js',

  // 使用生产模式。
  // 生产模式会启用一系列默认优化,包括代码压缩。
  mode: 'production',

  output: {
    // 输出文件名。
    filename: 'bundle.js',

    // 输出目录。
    path: path.resolve(__dirname, 'dist'),

    // 每次构建前清理 dist。
    clean: true,
  },

  optimization: {
    // 分析 ES Module 的 export 使用情况。
    //
    // 例如:
    // add       → 被使用
    // subtract  → 未使用
    // multiply  → 未使用
    //
    // Webpack 会对这些信息进行标记。
    usedExports: true,

    // 开启最终代码压缩。
    //
    // 注意:
    // usedExports 主要负责"分析和标记",
    // 压缩器负责进一步删除已经确定无用的代码。
    minimize: true,
  },
};

最终可以理解为:

text 复制代码
math.js
│
├── add       ──────→ 使用 → 保留
├── subtract  ──────→ 未使用 → 标记 → 删除
└── multiply  ──────→ 未使用 → 标记 → 删除

13. Tree Shaking 最容易被面试官追问的几个问题

问题 1:Tree Shaking 是不是 Webpack 独有?

不是。

Tree Shaking 是一种无用代码消除思想,现代前端构建工具普遍支持类似能力,例如:

text 复制代码
Webpack
Rollup
Rspack
esbuild
其他现代 JavaScript 构建工具

具体实现机制和优化程度可能不同。


问题 2:usedExports: true 就等于 Tree Shaking 吗?

不能简单画等号。

更准确:

text 复制代码
usedExports
→ 分析 export 使用情况
→ 标记未使用 export

代码压缩器
→ 根据标记删除 Dead Code

所以完整理解应该是:

Tree Shaking 是一套无用代码消除机制,usedExports 是 Webpack 实现其中"使用情况分析"的重要配置。


问题 3:为什么开发环境看到没删除?

因为:

text 复制代码
开发环境
→ 更关注构建速度、调试体验
→ 不一定进行完整压缩

生产环境
→ 更关注最终 Bundle 体积
→ 通常进行 Tree Shaking + Minification

所以不要仅仅看开发环境输出文件判断 Tree Shaking 是否有效。


问题 4:Tree Shaking 能不能删除所有没有执行的代码?

不能。

核心限制就是:

不能在无法证明"删除后不会改变程序行为"的情况下随意删除代码。

尤其需要关注:

text 复制代码
副作用
动态依赖
CommonJS
全局变量修改
原型修改
模块初始化
CSS
Polyfill

14. 这道题的真正考点

可以把整道题压缩成四层:

text 复制代码
第一层:概念
Tree Shaking = 消除无用代码

        ↓

第二层:为什么能做
ES Module 的 import/export 具有静态结构

        ↓

第三层:Webpack 怎么做
usedExports
→ 分析 export 使用情况
→ 标记未使用代码

        ↓

第四层:为什么能安全删除
sideEffects + 静态依赖分析 + 代码压缩

        ↓

最终
减少 Bundle 体积

主要矛盾

如何准确判断哪些代码既没有被使用,又可以安全删除。

次要矛盾

usedExports、sideEffects、压缩器分别承担什么职责,以及 ES Module / CommonJS 对静态分析能力的影响。


满分答案

面试题:Webpack 如何实现 Tree Shaking?

核心思路:

Tree Shaking 就是在构建阶段利用 ES Module 的静态结构分析模块依赖和 export 的使用情况,标记未使用代码,再结合副作用分析和代码压缩,把可以安全删除的无用代码从最终 Bundle 中移除。

流程图

text 复制代码
ES Module 源代码
      │
      ▼
分析 import / export
      │
      ▼
建立模块依赖关系
      │
      ▼
分析 export 使用情况
      │
      ▼
usedExports 标记未使用代码
      │
      ├── 有副作用 ──→ 不能随意删除
      │
      └── 无副作用 ──→ 可以进入删除阶段
                            │
                            ▼
                     Terser 等压缩器
                            │
                            ▼
                     删除无用代码
                            │
                            ▼
                      更小的 Bundle

底层原理

例如:

js 复制代码
// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}
js 复制代码
// main.js
import { add } from './math.js';

console.log(add(1, 2));

Webpack 能在构建阶段静态分析:

text 复制代码
main.js
  │
  └── 使用 math.js 的 add

math.js
  ├── add       → 使用 → 保留
  └── subtract  → 未使用 → 标记 → 压缩阶段删除

Webpack 中:

js 复制代码
optimization: {
  usedExports: true,
  minimize: true,
}

其中 usedExports 主要负责分析和标记哪些 export 被使用,而代码压缩器负责真正删除能够安全删除的代码。

为什么 ES Module 更适合 Tree Shaking?

因为:

js 复制代码
import { add } from './math.js';

属于静态模块依赖,构建阶段就可以知道依赖关系。

而:

js 复制代码
const name = getModuleName();
require(name);

模块在运行时才能确定,静态分析能力会受到限制。

还要注意副作用

例如:

js 复制代码
// polyfill.js
Array.prototype.foo = function () {};

即使没有使用任何 export,这个模块执行本身也会修改全局对象,因此不能简单删除。

所以还需要正确配置:

json 复制代码
{
  "sideEffects": false
}

或者明确声明有副作用的文件:

json 复制代码
{
  "sideEffects": [
    "*.css",
    "./src/polyfill.js"
  ]
}

这里最容易犯的错误是把 usedExports 理解成"直接删除无用代码"。更准确地说,usedExports 负责使用情况分析和标记,最终的代码删除通常由压缩阶段完成。

和代码分割的区别

text 复制代码
Tree Shaking
→ 删除不需要的代码
→ 减少代码总量

Code Splitting
→ 把代码拆成多个 Chunk
→ 减少单次加载量

两者可以配合使用。

一句话总结

Webpack Tree Shaking 的本质就是:利用 ES Module 的静态依赖关系,分析哪些导出没有被使用,再结合副作用分析和代码压缩,把确定无用的代码从最终 Bundle 中删除,从而减少 JavaScript 体积。

相关推荐
去伪存真1 小时前
从乱码到高精度检索:探矿业务中 TXT、Word、PDF 与网页的 RAG 清洗之道
前端·agent
两个西柚呀2 小时前
JS事件总结
前端·javascript·css
小狼154542 小时前
拼多多订单数据导出 CSV 实战:接口分页、字段平铺与 5 个数据坑,多多开票助手
java·前端·javascript
搬砖的小码农_Sky2 小时前
AI Agent:Node.js和浏览器运行环境的本质区别
javascript·node.js·人机交互
ss2732 小时前
AI全栈实战 | 2.2-02 React 思维模型:数据不可变 vs Vue 数据可变,两种哲学把复杂度推给谁
前端·vue.js·react.js
天衍四九-2 小时前
Docker Compose企业实战系列(三):Vue/React前端项目容器化完整部署
前端·vue.js·docker
Rosanci2 小时前
从零到合入主线:我的 DeepSeek Harness 开源贡献实战手记
开发语言·前端·开源
用户960819842232 小时前
多周期联看时十字光标对不齐:前端排查「时间桶」的三步清单
前端
Data analyse4563 小时前
数据合规的敏感数据怎么识别?
前端·人工智能·数据分析