第4章:资源压缩与合并 — 让文件“瘦身”的艺术
说到前端性能优化,资源压缩与合并绝对是最立竿见影的手段之一。我个人习惯把这一步叫做“给文件减肥”——你想想看,用户打开你的页面,要下载一堆没必要的空格、注释、重复代码,这多浪费带宽啊。
我在项目中遇到过不少团队,代码写得挺好,但上线前啥压缩都不做。结果首屏加载慢得离谱。其实只要花几分钟配置一下,加载速度能提升30%以上。今天我们就来聊聊这件事。
4.1 Gzip vs Brotli:选哪个?
先说传输层的压缩。HTTP协议支持两种主流压缩算法:Gzip和Brotli。
Gzip是老牌选手,几乎所有服务器和浏览器都支持。它的压缩率大概在60%-70%左右。说白了,一个100KB的JS文件,压缩完可能就30-40KB。
Brotli是Google推出的新算法。压缩率比Gzip高10%-20%。我在一个React项目里做过对比:Gzip压缩后是42KB,Brotli只有34KB。差距还是挺明显的。
核心结论:如果条件允许,优先用Brotli。但要注意,Brotli对CPU消耗略高,不过现代服务器完全扛得住。
配置示例(Nginx):
# 启用Gzip
gzip on;
gzip_types text/plain text/css application/javascript;
gzip_min_length 1024;
# 启用Brotli(需要安装brotli模块)
brotli on;
brotli_types text/plain text/css application/javascript;
brotli_min_length 1024;
小技巧:我建议同时开启Gzip和Brotli。浏览器会优先选择Brotli,不支持的话自动降级到Gzip。这样兼容性最好。
4.2 JS/CSS/HTML压缩:去掉没用的东西
传输层压缩搞定了,接下来是代码本身的压缩。这一步去掉的是:空格、换行、注释、长变量名。
JavaScript压缩,我推荐用TerserWebpackPlugin。它不仅能压缩,还能做一些死代码删除。举个例子:
// 压缩前
function add(a, b) {
// 这是一个加法函数
return a + b;
}
const result = add(1, 2);
console.log(result);
// 压缩后
function add(a,b){return a+b}console.log(add(1,2));
你看,注释没了,空格没了,变量名也没变短(因为函数名可能被外部引用)。但整体体积小了很多。
CSS压缩,我用css-minimizer-webpack-plugin。它会把颜色值从#ffffff变成#fff,去掉不必要的分号。嗯,这里要注意:有些CSS hack写法会被误删,压缩前最好做一次兼容性测试。
HTML压缩,用html-webpack-plugin配合html-minifier-terser。它会去掉HTML里的空格和注释。不过我个人觉得HTML压缩收益不大,因为HTML本身就不大。但如果你有大量内联脚本或样式,那还是值得做的。
避坑指南:我曾经在压缩CSS时,把calc(100% - 20px)里的空格去掉了,结果样式全乱了。后来才知道,calc里的运算符前后必须留空格。所以压缩配置里要保留这部分空格。
4.3 代码合并:减少HTTP请求
HTTP/1.1时代,每个请求都有开销。合并文件能显著减少请求数。但HTTP/2时代,这个策略需要调整。
为什么?因为HTTP/2支持多路复用。一个连接可以同时发送多个请求。合并文件反而可能破坏缓存策略——你改了一行代码,整个大文件都得重新下载。
我个人现在的做法是:
- 基础库合并:把React、Vue这类不常变的库合并成一个vendor.js
- 业务代码按路由拆分:每个页面单独打包,不合并
- 小文件合并:小于1KB的图标、工具函数可以合并,减少请求数
Webpack配置示例:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
},
};
4.4 Tree Shaking:干掉没用的代码
Tree Shaking,直译是“摇树”。你摇一下树,枯枝败叶就掉下来了。代码里也一样——那些没被引用的函数、组件,都应该被摇掉。
它依赖ES Module的静态结构。说白了,import和export是静态的,编译器能分析出哪些代码被用了,哪些没用。
举个例子:
// utils.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// main.js
import { add } from './utils';
console.log(add(1, 2));
Tree Shaking之后,subtract函数会被移除。因为main.js里根本没用到它。
关键点:Tree Shaking只在生产模式下生效。开发模式为了调试方便,不会做这个优化。
但要注意几个坑:
- 副作用:有些模块导入后会产生副作用(比如修改全局变量)。Webpack默认认为所有模块都有副作用,所以不会摇掉它们。你需要在
package.json里声明"sideEffects": false。 - CommonJS:
require是动态的,没法做Tree Shaking。所以库作者最好提供ES Module版本。 - CSS:CSS的Tree Shaking比较麻烦。我一般用PurgeCSS,它会扫描你的HTML和JS,找出没用的CSS类名并删除。
我的经验:曾经有个项目,打包后vendor.js有2MB。排查发现,是因为引入了整个lodash库,但只用到了_.debounce。后来改成按需引入import debounce from 'lodash/debounce',体积直接降到200KB。Tree Shaking配合按需引入,效果翻倍。
4.5 实战:一个完整的压缩配置
最后,给你一个我常用的生产环境Webpack配置。它集成了压缩、合并、Tree Shaking:
// webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
const glob = require('glob');
const path = require('path');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 去掉console.log
},
},
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10,
},
},
},
},
plugins: [
new PurgeCSSPlugin({
paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, { nodir: true }),
}),
],
};
这个配置做了三件事:
- 压缩JS和CSS,去掉console.log
- 把第三方库合并到vendor.js
- 用PurgeCSS清理没用到的CSS
嗯,基本上够用了。如果你用Vite,配置会更简单——它默认就开启了Tree Shaking和压缩。
资源压缩与合并,说白了就是“少发、发小、发精”。少发请求,发小文件,发精内容。这三步做好了,页面加载速度至少提升50%。不信你试试看。