tree shaking 无法剔除导出对象中未使用的属性,因其以“绑定”为单位分析模块导出,而非对象内部字段;js 属性访问的动态性使静态分析无法安全追踪,故只要对象被引用就保留全部属性。

Tree Shaking 无法单独剔除导出对象中未被读取的属性,根本原因在于:**ES 模块的导出单位是“绑定”(binding),不是“属性”(property)**。打包工具分析的是模块顶层的 export 语句,而非对象内部字段的使用情况。
导出的是整个对象绑定,不是它的字段
当你写:
export const utils = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b
};
你实际导出的是一个名为 utils 的常量绑定,它指向一个对象。Tree Shaking 工具(如 Webpack、Rollup、Vite)能判断的是:“utils 这个绑定是否被 import 并使用”,而不是 “utils.multiply 是否被访问过”。只要代码里有 import { utils } from './utils' 或 import * as mod from './utils',整个对象就会被保留。
静态分析无法追踪对象属性访问路径
JS 中对象属性访问(obj.x)是动态的,即使看起来是字面量,也可能被混淆、重命名、代理拦截或运行时计算。例如:
-
utils[methodName]—— 属性名来自变量 -
Object.keys(utils).forEach(...)—— 枚举全部属性 -
new Proxy(utils, {...})—— 拦截所有读写
这些行为在构建期无法被 100% 安全推断。为避免破坏运行时行为,打包工具选择保守策略:**只要对象被引用,就保留其全部属性**。
正确做法:用命名导出替代对象聚合
把每个能力拆成独立的命名导出,才能让 Tree Shaking 精准生效:
// ✅ 可摇:每个函数都是独立导出绑定
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
这样,如果只 import { add } from './utils',其余两个函数就能被安全剔除。
补充说明:sideEffects 和 module 配置不解决这个问题
"sideEffects": false 只告诉打包工具“该模块没有副作用”,允许整模块被删;但它不改变“对象属性不可单独剔除”这一限制。同样,即使库提供了 ESM 版本("module": "dist/index.mjs"),若仍用对象方式导出,内部字段也无法被摇掉。











