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 整体流程与工具链

下面这张图,是我自己总结的「生产环境资源压缩流程」。你看一遍就能理解整个链路:

生产环境资源压缩流程 生产环境资源压缩流程 源码(JS / CSS) Tree Shaking(摇树优化) 代码压缩与混淆 Terser(JS) CSSNano(CSS) 打包输出(bundle) 部署到生产环境 💡 建议顺序: 先 Tree Shaking,再压缩 顺序错了可能多出 20% 体积

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,合并简写属性,颜色值压缩

嗯,这一章的内容就这些。配置不难,但细节不少。你动手配一遍,遇到问题再回来看这张表,基本就能搞定。