只 import 了一个函数,另一个函数为什么还出现在打包文件里?排查 Tree Shaking 时,需要看构建输入和最终产物,而不是看到 export 就默认其他代码都能被删掉。
ES Module 提供静态的导入导出关系,构建工具可以分析哪些导出被使用。分析出没用的代码,以及最后删掉这些代码,也可能发生在不同阶段。
用一个标记检查生产产物
在空目录运行 npm init -y 和 npm install —save-dev —save-exact webpack@5.99.9。下面用 Node API 构建,不依赖全局 webpack 命令。
创建 math.js:
export const used = (a, b) => a + b;
export function unused() {
return "UNUSED_MARKER_FOR_EXPERIMENT";
}
创建 entry.js,只调用 used:
import { used } from "./math.js";
globalThis.result = used(2, 3);
创建 build.cjs:
const webpack = require("webpack");
const path = require("path");
webpack(
{
mode: "production",
entry: path.resolve("entry.js"),
output: {
path: path.resolve("dist"),
filename: "main.js",
chunkFilename: "[id].js",
},
devtool: false,
optimization: { usedExports: true },
},
(error, stats) => {
if (error || stats.hasErrors()) {
console.error(error || stats.toString("errors-only"));
process.exitCode = 1;
return;
}
console.log(
JSON.stringify(
stats.toJson({
all: false,
assets: true,
chunks: true,
modules: true,
usedExports: true,
}),
null,
2,
),
);
},
);
运行 node build.cjs。再创建 check.cjs,检查真正输出的 JS:
const fs = require("fs");
const assert = require("assert").strict;
const vm = require("vm");
const code = fs.readFileSync("dist/main.js", "utf8");
assert.ok(!code.includes("UNUSED_MARKER_FOR_EXPERIMENT"));
const context = {};
vm.runInNewContext(code, context);
assert.equal(context.result, 5);
console.log("未使用函数已移除,使用的函数仍然正常");
这个最小例子里,生产产物没有 marker,执行结果仍然是 5。VM 只用来执行自己写的固定产物,不作为运行未知代码的安全沙箱。
顶层代码不能只看有没有导出
模块里如果有 console.log、全局注册或者样式导入,即使导出的变量没人读,加载模块仍可能有作用。不能把“导出没用”直接当成“整个模块能删”。
sideEffects: false 是对文件副作用的声明,不是可以给所有包开启的万能优化。带 CSS 或初始化代码的文件被错误标记时,生产构建可能丢掉需要的行为。
把这些配置加上以前,需要确认输入还保留 ESM。Babel 提前转换成 CommonJS、动态访问导出,或者库本身提供的入口不同,都可能影响分析。
看到代码还在,检查的对象要对
开发构建为了调试,可能保留未压缩的代码;source map 也可能包含原始源码。map 的 sourcesContent 出现函数,不等于部署的 JS 还在执行它。
可以从生产 JS、usedExports 和模块副作用一步步查。这个例子没有比较体积百分比,只检查不需要的函数消失、需要的行为保留。配置说明参考 Webpack Tree Shaking 文档。