Javascript is required
错误排查发布于 2026-07-28更新于 2026-08-08审校于 2026-08-086 分钟阅读

JavaScript 压缩与 DCE 优化:Bundler 模块图、Minifier 副作用判定与 /*@__PURE__*/ 实践

在现代 JavaScript 构建流水线中,打包工具 (Bundler) 负责模块拓扑树的 Tree Shaking,而压缩工具 (Minifier) 负责单个语法树层面的死代码消除 (Dead Code Elimination, DCE) 与代码混淆。当未显式调用的初始化逻辑或函数被压缩工具静默擦除时,根源往往在于工具无法确定调用是否具备副作用。本文澄清打包与压缩的职责界限,并给出副作用标注方法。

TerserJS MinificationSide EffectsTree Shaking Bug

一、问题概述:Bundler 与 Minifier 优化职责的重叠与误解

开发者常把 Tree Shaking 的失败或代码被误删归咎于 Terser 或 esbuild。实际上,Rollup / Webpack 等 Bundler 决定“哪些模块与导出需要包含在 Bundle 中”,而 Terser 等 Minifier 决定“已包含在 AST 节点中的哪些未引用变量或函数调用可以安全擦除”。如果一个函数调用可能修改全局 window、执行原型链挂载或读取 Getter,Minifier 必须保守假设其具备副作用并予以保留;误判定则会导致无用代码残留或正常逻辑丢失。

二、最小复现:未被引用的函数调用因隐式副作用残留或误删

下面的代码中,createLogger 仅创建内部实例,无全局修改;但在未添加 PURE 标记时,压缩器不敢贸然删除未引用的变量赋值。

/* 源码逻辑:未引用的变量赋值 */
const logger = createLogger({ level: 'info' });

/* 无 PURE 标记时:压缩器保留该表达式,因为 createLogger 可能修改外部状态 */
createLogger({ level: 'info' });

/* 添加 PURE 标记后:压缩工具安全将其判定为无副作用表达式并直接擦除 */
const logger = /*@__PURE__*/ createLogger({ level: 'info' });

三、根因分析:JavaScript 动态特性的副作用推导局限

JavaScript 是强动态语言,全局变量修改、属性 Getter 触发、Object.defineProperty 与类构造函数都可能产生不可预测的运行时改变。Terser 在进行 DCE 时,采用保守的语义分析。只要函数调用包含参数传入或无法证明纯净的函数引用,即便其返回值未被使用,默认也不会将其丢弃。

四、推荐方案:显式 /*@__PURE__*/ 标注与库代码设计规范

1. 库作者规范:在导出的顶层对象创建或工具函数调用前增加 /*@__PURE__*//*#__PURE__*/ 注释,告知 Bundler 与 Minifier 该调用不包含全局副作用。2. 业务代码规范:避免在模块顶层执行隐式副作用(如直接改写全局对象原型),将初始化逻辑收拢至显式 init() 函数中。3. Terser 配置:合理配置 compress.pure_funcs(指定特定 API 函数名)与 side_effects 编译参数。

五、完整代码:模拟 Minifier 副作用判定与 DCE 擦除逻辑

下面的 TypeScript 代码模拟代码压缩器如何根据变量引用计数、隐式全局修改标志以及 PURE 标记来决定是否安全擦除未调用的表达式。

interface FunctionCallNode {
  name: string;
  hasPureAnnotation: boolean;
  modifiesGlobal: boolean;
}

function evaluateDeadCodeElimination(node: FunctionCallNode, isReferenced: boolean): boolean {
  // 如果变量被显式引用,必须保留
  if (isReferenced) return false;
  // 如果显式标记为 PURE 且没有隐式全局修改,可安全擦除 (DCE)
  if (node.hasPureAnnotation && !node.modifiesGlobal) return true;
  // 未标记 PURE 时,无法保证调用的副作用,必须保守保留
  return false;
}

const callA: FunctionCallNode = { name: "createLogger", hasPureAnnotation: true, modifiesGlobal: false };
const callB: FunctionCallNode = { name: "setupGlobalState", hasPureAnnotation: false, modifiesGlobal: true };

console.log(evaluateDeadCodeElimination(callA, false)); // true (安全擦除未调用的纯函数)
console.log(evaluateDeadCodeElimination(callB, false)); // false (保守保留具备副作用的函数)

六、常见错误方案

盲目开启 Terser 强力优化参数(如 unsafe)导致有状态函数被错误剔除;在模块顶层随意挂载全局副作用却期待工具自动消除;企图仅靠 Minifier 完成多文件级别的 Tree Shaking。

七、边界条件:IIFE 立即执行函数与 Getter 拦截

立即执行函数 (IIFE) 内部的全局改动无法被自动分析;对访问器属性 (Getter/Setter) 的求值调用始终被视作具备潜在副作用。

八、如何验证 DCE 优化安全性

检查产物中目标代码是否按预期消除;在未压缩与压缩构建上运行同一组初始化、副作用和动态导入测试,并比较关键运行输出。

九、FAQ

问:Bundler 的 Tree Shaking 和 Minifier 的 DCE 有何不同?答:Bundler 根据 ES Module 的 import/export 模块图剔除文件间未引用的导出;Minifier 在单 AST 层面剥离表达式、局部变量与死分支。

问:/*@__PURE__*/ 注释对所有压缩工具都有效吗?答:Terser、esbuild、Rollup、SWC 等主流工具均支持该标准注释。

问:为什么加了 PURE 标记函数仍未被擦除?答:如果函数的返回值被后续生效代码引用,或参数本身包含其它非 Pure 的求值表达式,仍会被保留。

十、总结

明确 Bundler 模块图分析与 Minifier AST DCE 的职责分工。在无副作用的纯函数调用处显式声明 PURE 标注,并收拢顶层全局修改,才能安全最大化 JavaScript 瘦身效果。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具