Tree Shaking 无法移除全局挂载代码,因其被视为副作用;应改用纯 ESM 导出 + 独立适配层挂载,或用 /*#__PURE__*/ 标记可控场景。

Tree Shaking 无法自动移除挂载到全局命名空间(如 window、globalThis 或自定义全局对象)上的属性,因为这类赋值被视为“有副作用”——打包工具(如 Webpack、Rollup、Vite)默认保守处理,不敢擅自删除可能被外部代码依赖的全局暴露行为。
为什么全局挂载会阻断 Tree Shaking
当模块执行类似 window.MyLib = { foo() {} }; 或 globalThis.utils = helpers; 的操作时:
- 打包器无法静态分析该对象是否在模块外被读取(例如浏览器控制台、第三方脚本、旧版插件)
- ESM 的静态导入/导出机制不覆盖运行时全局赋值,因此这些语句不会被识别为“未使用导出”
- 即使模块其他部分完全未被引用,只要存在全局挂载语句,整个模块都会被保留
安全暴露 API 而不破坏摇树的常用方式
核心原则:把“是否挂载”从模块逻辑中解耦,交由入口或配置层决定。
-
只用标准 ESM 导出:模块内部不碰全局对象,仅通过
export提供功能,例如:
utils.jsexport function debounce(fn) { ... }<br>export const throttle = (fn) => { ... }; -
单独创建“UMD/全局适配层”:在独立入口文件(如
dist/mylib.global.js)中显式挂载:import * as MyLib from './utils.js';<br>if (typeof window !== 'undefined') window.MyLib = MyLib;
-
利用打包工具的库模式(library mode):在 Webpack/Vite 中配置
build.lib或output.library,让工具自动生成带 UMD/Global 包裹的产物,而非在源码里手动挂载
如果必须动态挂载,如何最小化影响
仅在明确需要且可控的场景下使用,同时主动告知打包器“这段是无副作用的”:
立即学习“Java免费学习笔记(深入)”;
-
用注释标记纯副作用(Rollup/Webpack 支持):
/*#__PURE__*/ window.MyWidget = createWidget();—— 告诉工具:这行可被安全删除,只要其右侧表达式本身无副作用 -
避免条件外挂载:不要写
if (DEBUG) window.debugApi = ...,这种分支会让工具难以判断;改用构建时常量替换(如import.meta.env.DEV)并配合/*#__PURE__*/ -
不混用模块导出与全局挂载:同一个模块既
export又window.X = ...,会导致 Tree Shaking 失效——拆分成 pure 模块 + adapter 模块更清晰
检查是否真的被摇掉的小技巧
验证实际效果比理论更重要:
- 用
rollup --treeshake=verbose或 Webpack 的stats: 'verbose'查看哪些模块被标记为 “unused exports” - 构建后搜索产物文件中是否存在你认为该被删掉的函数名或字符串字面量
- 在 Vite 中启用
build.reportCompressedSize: false并配合vite-bundle-visualizer插件直观查看依赖图


















