前端一面:打包、模块化与 Tree-Shaking 考点

工程化相关的一面题,常见会问:ESM 和 CommonJS 有什么区别?打包工具在做什么?Tree-Shaking 的前提条件是什么?这一篇尝试用一条线把这些高频问题串起来。

在浏览器/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、Rollup、Vite 内部的 Rollup 等)主要负责几件事:

  • 解析模块依赖图(从入口文件出发,追踪 import/require);
  • 把多种资源类型(JS/TS/CSS/图片等)转成浏览器可执行的 bundle;
  • 支持代码分割(Code Splitting)、按需加载等优化;
  • 在生产模式下做压缩、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。

参考答案要点:

  • 语法上:ESM 用 import/export,CommonJS 用 require/module.exports
  • 语义上:ESM 是静态的,编译阶段就能确定依赖和导出;
  • Tree-Shaking 需要依赖静态分析来判断哪些导出从未被使用,因此 ESM 更适合作为基础。

可以补一句:

“在 Node 里现在也逐步支持 ESM,但老项目和很多库仍然以 CommonJS 为主,经常需要做兼容。”

参考答案思路:

  • 打包工具会从入口文件出发构建依赖图,把源码和各种资源打包成浏览器可执行的形式;
  • 开发环境侧重于:
    • 快速构建(有时是原生 ESM + 按需编译);
    • 热更新和调试体验;
  • 生产环境侧重于:
    • 体积优化(压缩、Tree-Shaking、Code Splitting);
    • 缓存友好(文件名哈希、长期缓存策略)。

这一题重点是让你表现出“知道为什么要有构建/打包”以及“环境差异”的意识。

参考答案要点:

  • 理论上只能移除构建时可证明“从未被使用”的代码;
  • 动态 require、给全局对象挂属性、带副作用的导入等会妨碍 Tree-Shaking;
  • 某些库如果没有正确标注 sideEffects,打包器会保守地保留代码。

可以补充一个工程实践点:

“我们在项目里通常会尽量用 ESM 形式的导入,并对有副作用的文件进行清晰标注,方便打包器更激进地 Tree-Shake。”