前端一面:打包、模块化与 Tree-Shaking 考点
工程化相关的一面题,常见会问:ESM 和 CommonJS 有什么区别?打包工具在做什么?Tree-Shaking 的前提条件是什么?这一篇尝试用一条线把这些高频问题串起来。
模块化:ESM vs CommonJS
在浏览器/Node 里,常见的两套模块规范:
ES Module(ESM)
- 使用
import/export语法; - 静态结构(编译时即可分析依赖关系),有助于 Tree-Shaking;
- 默认是异步加载(浏览器原生支持
<script type="module">)。
- 使用
CommonJS(CJS)
- 使用
require/module.exports; - 动态特性更强(
require可以在条件里调用),但不利于静态分析; - Node 传统模块系统。
- 使用
一面典型问法:
- “ESM 和 CommonJS 有什么区别?”
- “为什么 Tree-Shaking 更适合用在 ESM 上?”
参考答案要点:
- ESM 是静态的,编译阶段就能分析出哪些导出被使用;
- CommonJS 的导出是一个对象,属性可以在运行时动态增删,难以安全删除未用代码;
- 实际打包时,通常推荐业务代码用 ESM,第三方库兼容两种模块格式。
打包工具在做什么?(以 Webpack/Vite 为例)
打包工具(Webpack、Rollup、Vite 内部的 Rollup 等)主要负责几件事:
- 解析模块依赖图(从入口文件出发,追踪 import/require);
- 把多种资源类型(JS/TS/CSS/图片等)转成浏览器可执行的 bundle;
- 支持代码分割(Code Splitting)、按需加载等优化;
- 在生产模式下做压缩、Tree-Shaking 等。
一面中不会要求你精通某个工具的所有配置,更多是看你是否理解:
- 为什么需要打包(兼容性、性能、模块组织);
- 打包前后代码在结构和加载策略上的变化。
Tree-Shaking:消除死代码的前提和局限
Tree-Shaking 通常指“在构建时移除未被使用的导出”,前提包括:
- 使用静态可分析的模块系统(ESM);
- 没有副作用的导入/导出(或者通过配置标注哪些模块有副作用);
- 构建工具正确识别并在压缩阶段配合删除。
典型例子:
1// utils.ts
2export function a() {}
3export function b() {}
4
5// entry.ts
6import { a } from "./utils";
7a();
如果构建链路配置正确,最终 bundle 中只应包含 a,而不是整个 utils。
可以顺带提到一些实际踩坑点:
- 某些第三方库未标注 sideEffects 导致无法 Tree-Shake;
- 动态使用导出的场景(如给 window 挂载)会阻碍 Tree-Shaking。
常见面试题与参考答案
题 1:简单说下 ESM 和 CommonJS 的区别?为什么 ESM 更适合做 Tree-Shaking?
参考答案要点:
- 语法上:ESM 用
import/export,CommonJS 用require/module.exports; - 语义上:ESM 是静态的,编译阶段就能确定依赖和导出;
- Tree-Shaking 需要依赖静态分析来判断哪些导出从未被使用,因此 ESM 更适合作为基础。
可以补一句:
“在 Node 里现在也逐步支持 ESM,但老项目和很多库仍然以 CommonJS 为主,经常需要做兼容。”
题 2:打包工具的主要工作是什么?开发环境和生产环境有什么差异?
参考答案思路:
- 打包工具会从入口文件出发构建依赖图,把源码和各种资源打包成浏览器可执行的形式;
- 开发环境侧重于:
- 快速构建(有时是原生 ESM + 按需编译);
- 热更新和调试体验;
- 生产环境侧重于:
- 体积优化(压缩、Tree-Shaking、Code Splitting);
- 缓存友好(文件名哈希、长期缓存策略)。
这一题重点是让你表现出“知道为什么要有构建/打包”以及“环境差异”的意识。
题 3:Tree-Shaking 一定能移除所有未使用代码吗?有没有失败的情况?
参考答案要点:
- 理论上只能移除构建时可证明“从未被使用”的代码;
- 动态 require、给全局对象挂属性、带副作用的导入等会妨碍 Tree-Shaking;
- 某些库如果没有正确标注 sideEffects,打包器会保守地保留代码。
可以补充一个工程实践点:
“我们在项目里通常会尽量用 ESM 形式的导入,并对有副作用的文件进行清晰标注,方便打包器更激进地 Tree-Shake。”