构建期性能拦截:基于 AST 与构建插件的自动化瘦身实战
前端性能优化常陷入“先污染后治理”的怪圈:业务代码随意引入重型依赖,上线后才发现包体积暴涨、运行时卡顿。真正的性能治理应前置到构建期,利用工程化手段建立拦截机制。本文通过三个实战场景,演示如何基于 AST 与构建插件实现自动化瘦身。
一、依赖拦截:拒绝重型库“偷渡”
业务开发中,moment、lodash 等库常被随手引入,导致包体积瞬间失控。运行时提醒不如构建期报错。
反例:运行时无感知,构建后体积暴涨
import moment from 'moment'; // 引入后增加约 250KB gzip
import _ from 'lodash'; // 引入后增加约 70KB gzip
const time = moment().format('YYYY-MM-DD');
const data = _.cloneDeep(obj);
正例:Webpack 插件构建期硬拦截
编写 DependencyGuardPlugin,在模块解析阶段拦截黑名单依赖并抛出错误。
class DependencyGuardPlugin {
constructor(options) {
this.blacklist = options.blacklist; // e.g. [/^moment$/, /^lodash$/]
}
apply(compiler) {
compiler.hooks.compilation.tap('DependencyGuardPlugin', (compilation) => {
// 在模块构建完成时检查
compilation.hooks.optimizeModules.tap('DependencyGuardPlugin', (modules) => {
for (const module of modules) {
if (module.rawRequest && this.blacklist.some(reg => reg.test(module.rawRequest))) {
compilation.errors.push(
new Error(`[DependencyGuard] 禁止引入 ${module.rawRequest},请使用 dayjs 或 lodash-es 替代`)
);
}
}
});
});
}
}
// webpack.config.js
module.exports = {
plugins: [
new DependencyGuardPlugin({ blacklist: [/^moment$/, /^lodash$/] })
]
};
构建直接失败,将性能隐患扼杀在摇篮中。
二、常量预计算:将运行时消耗转移至编译期
复杂的静态配置或计算逻辑,若在运行时执行,会阻塞主线程。通过 Babel 插件,可将特定函数调用在编译期预计算并内联结果。
反例:运行时重复计算静态配置
function generateHeavyConfig(env) {
// 复杂计算逻辑,耗时 50ms
let config = {};
for (let i = 0; i < 10000; i++) {
config[`key_${i}`] = `${env}_value_${Math.random()}`;
}
return config;
}
// 每次页面加载都需执行
const APP_CONFIG = generateHeavyConfig('production');
正例:Babel 插件编译期求值并替换
插件识别 generateHeavyConfig 调用,在 Node 环境执行函数,将返回值序列化为 AST 节点替换原调用。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
CallExpression(path) {
const { callee, arguments: args } = path.node;
// 匹配目标函数名且参数均为字符串字面量
if (t.isIdentifier(callee, { name: 'generateHeavyConfig' })
&& args.every(arg => t.isStringLiteral(arg))) {
// 在编译期执行函数
const evaluatedValue = generateHeavyConfig(args[0].value);
// 将 JS 值转换为 AST JSON 字面量节点
const astNode = t.valueToNode(evaluatedValue);
path.replaceWith(astNode);
}
}
}
};
};
编译后产物直接输出 JSON 对象,运行时消耗降为 0。
// 编译产物
const APP_CONFIG = { "key_0": "production_value_0.123", ... };
三、副作用精准剔除:突破 Tree-Shaking 盲区
Webpack 的 Tree-Shaking 依赖 package.json 的 sideEffects 字段,但面对模块内部隐式副作用常显得无力。通过自定义插件修改模块元信息,可强制剥离无用代码。
反例:隐式副作用导致 Tree-Shaking 失效
// utils.js
export function usedUtil() { return 1; }
export function unusedUtil() { return 2; }
// 隐式副作用:修改全局原型
Array.prototype.customMethod = function() {};
即使只导入 usedUtil,unusedUtil 和原型修改代码也无法被摇掉。
正例:构建期重写模块副作用标记
编写 Webpack 插件,针对特定模块强制标记无副作用,让 Terser 大胆删除无用导出。
class ForceSideEffectsPlugin {
constructor(options) {
this.includes = options.includes; // 需要强制标记的模块路径
}
apply(compiler) {
compiler.hooks.compilation.tap('ForceSideEffectsPlugin', (compilation) => {
compilation.hooks.optimizeModules.tap('ForceSideEffectsPlugin', (modules) => {
for (const module of modules) {
if (this.includes.test(module.resource)) {
// 强制覆盖模块的副作用标记
module.buildMeta.sideEffects = false;
}
}
});
});
}
}
// webpack.config.js
module.exports = {
plugins: [
new ForceSideEffectsPlugin({ includes: /src\/utils\.js$/ })
]
};
标记后,Terser 将安全地移除 unusedUtil,仅保留被引用的代码,实现包体积的深度压缩。
结语
性能优化不应仅靠人工 Review 和事后补救。通过构建插件拦截依赖、AST 预计算常量、精准控制副作用,我们能在工程化链路上建立自动化的性能防线。将性能约束内置于构建流程中,才是工程化治本之道。
评论区
登录 后参与评论