4、代码混淆与压缩:JavaScript/CSS 的 Tree Shaking、Uglify、CSSNano
代码压缩这件事,说白了就是「把能省的字符全省掉」。
我早年做项目时,打包出来的 JS 文件动不动就 1MB 多。用户加载慢,老板天天催。后来我才意识到——不是代码写多了,是没做压缩和混淆。这一章,咱们就把 Tree Shaking、Uglify(现在更常用 Terser)、CSSNano 这几个核心工具讲透。
4.1 为什么需要代码混淆与压缩?
你想想看,浏览器执行代码时,它才不在乎变量名叫 a 还是 userName。空格、换行、注释,浏览器统统忽略。那这些字符留在生产环境里干嘛?
压缩的核心目标就三个:
- 减小体积:去掉无用字符,文件更小,加载更快
- 混淆保护:变量名缩短,增加逆向难度
- Tree Shaking:砍掉没被引用的代码,只留真正用到的
一句话总结:开发时写给人看,生产时写给机器看。中间这步转换,就是压缩与混淆。
4.2 Tree Shaking:砍掉死代码
Tree Shaking 这个词挺形象——像摇一棵树,枯枝败叶全掉下来,只留健康的枝叶。
它依赖 ES Module 的静态结构。什么意思?import 和 export 是在编译时就确定的,不像 CommonJS 的 require 可以在运行时动态加载。静态就意味着工具能分析出哪些导出没有被引用,然后安全地删掉。
4.2.1 配置 Tree Shaking
在 Webpack 里,只要把 mode 设为 "production",Tree Shaking 默认就开了。但有个前提——你的代码必须用 ES Module 写法。
// webpack.config.js
module.exports = {
mode: 'production',
// Tree Shaking 自动启用
};
我个人习惯在 package.json 里加一个 "sideEffects" 字段。这玩意儿告诉打包工具:哪些文件有副作用,不能随便删。
{
"name": "my-project",
"sideEffects": [
"./src/polyfill.js",
"*.css"
]
}
小技巧:如果你确定项目里所有文件都没有副作用,直接写 "sideEffects": false。Tree Shaking 会更激进,体积能再小 10%-20%。
4.2.2 实际案例
我在项目中遇到过这样一个坑:引入了一个工具库,只用了其中一个函数,结果打包出来还是把整个库都带上了。
// ❌ 错误写法
import { debounce } from 'lodash';
// ✅ 正确写法
import debounce from 'lodash/debounce';
原因很简单——lodash 默认不是 ES Module 格式,Tree Shaking 对它无效。换成按路径导入,或者用 lodash-es,问题就解决了。
4.3 JavaScript 压缩:从 UglifyJS 到 Terser
UglifyJS 是老牌工具,但有个硬伤——不支持 ES6+ 语法。现在前端代码谁不用箭头函数、const、async/await?所以社区逐渐转向了 Terser,它本质上是 Uglify 的升级版,完全兼容 ES6+。
4.3.1 Terser 做了什么?
我举个例子,你一看就明白:
// 压缩前
function greet(name) {
const message = 'Hello, ' + name + '!';
console.log(message);
}
// 压缩后
function greet(n){console.log("Hello, "+n+"!")}
具体操作包括:
- 变量名缩短:
name→n - 删除空格和换行:一行搞定
- 常量折叠:
'Hello, ' + n + '!'保留,但有些场景会直接合并字符串 - 删除无用代码:比如
if(false){...}直接移除
4.3.2 配置 TerserPlugin
在 Webpack 里,TerserPlugin 是 production 模式默认的压缩器。如果你想自定义,可以这样配:
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 干掉所有 console.log
drop_debugger: true, // 干掉 debugger
},
mangle: {
toplevel: true, // 混淆顶层变量名
},
},
}),
],
},
};
注意:drop_console: true 会删掉所有 console 调用。如果你线上需要日志排查问题,千万别开。我曾经因为这个被坑过一次,线上报错查了半天,才发现日志全被干掉了。
4.4 CSS 压缩:CSSNano
CSS 压缩的逻辑和 JS 类似——去掉空格、注释、重复规则,合并选择器。CSSNano 是目前最主流的工具。
4.4.1 CSSNano 做了什么?
/* 压缩前 */
.container {
margin: 10px 10px 10px 10px;
background-color: #ffffff;
color: #ff0000;
}
/* 压缩后 */
.container{margin:10px;background-color:#fff;color:red}
具体优化点:
- 合并简写属性:
margin: 10px 10px 10px 10px→margin: 10px - 颜色值压缩:
#ffffff→#fff,#ff0000→red - 删除多余空格和注释
- 合并相同选择器
4.4.2 在 Webpack 中使用 CSSNano
通常配合 css-minimizer-webpack-plugin 使用:
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
},
};
它底层调用的就是 CSSNano。默认配置已经够用,但如果你想更激进,可以传 preset 参数:
new CssMinimizerPlugin({
minimizerOptions: {
preset: ['advanced', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
}],
},
}),
4.5 整体流程与工具链
下面这张图,是我自己总结的「生产环境资源压缩流程」。你看一遍就能理解整个链路:
4.6 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Tree Shaking 没生效 | 代码用了 CommonJS 或动态 import | 改用 ES Module 写法 |
| 压缩后报错 | Terser 不认识某些新语法 | 升级 Terser 版本,或加 Babel 转译 |
| CSS 压缩后样式丢失 | CSSNano 合并规则时出错 | 检查是否有重复选择器,或降级 preset |
| 打包体积没变小 | 第三方库未做 Tree Shaking | 检查库是否提供 ES Module 版本 |
我曾经踩过的坑:有一次升级依赖后,打包出来的 JS 文件突然大了 30%。排查了半天,发现是某个库从 ES Module 版本降级成了 CommonJS 版本。Tree Shaking 直接失效。从那以后,我每次升级依赖都会对比一下打包体积。
4.7 总结
代码混淆与压缩,是前端工程化里性价比极高的一步。你不需要写多少代码,配几个插件,体积就能砍掉 40%-60%。
核心就三件事:
- Tree Shaking:砍掉没用到的代码,前提是用 ES Module
- Terser:压缩 JS,支持 ES6+,记得关掉
drop_console除非你确定不需要 - CSSNano:压缩 CSS,合并简写属性,颜色值压缩
嗯,这一章的内容就这些。配置不难,但细节不少。你动手配一遍,遇到问题再回来看这张表,基本就能搞定。