为什么写这篇:2017 年 HTTP/2 全面铺开、vue-cli@3.4 默认开了很多性能优化(splitChunks、tree-shaking、terser)。本文把"代码 + 服务器 + 框架"三层优化串起来——一个真实项目把首屏从 10s 压到 2s。
适用读者:Vue 2/3 项目的性能优化负责人;想系统理解 HTTP 缓存 / CDN / gzip 的初中级前端。
前置知识:会用 vue-cli 启项目;知道 Nginx 配置文件长什么样。
目录
- 前端性能优化的三层模型
- 代码层面:CSS 位置与请求数
- 代码层面:减少 HTTP 请求
- 代码层面:CDN 与外部资源
- 代码层面:Gzip 压缩
- 服务器层面:Nginx gzip 配置
- 服务器层面:HTTP/2 多路复用
- 服务器层面:缓存策略
- Vue 实战:compression-webpack-plugin + externals + sourceMap
- 首屏时间从 10s 到 2s 的真实案例
1. 前端性能优化的三层模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| ┌─────────────────────────────────────┐
│ Layer 1:代码层面(HTML/CSS/JS) │
│ - 样式表放 head、脚本放 body 底部 │
│ - 雪碧图 / 内联小图 / 合并脚本 │
│ - 使用外部 JS/CSS(可缓存) │
│ - 使用 CDN 引入公共库 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Layer 2:服务器层面(Nginx) │
│ - 开启 gzip 压缩 │
│ - 开启 HTTP/2(需 HTTPS) │
│ - 强缓存(Expires / Cache-Control)│
│ - 协商缓存(ETag / Last-Modified)│
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Layer 3:Vue 专项(vue-cli 3+) │
│ - 拆 vendor(splitChunks) │
│ - externals 走 CDN │
│ - productionSourceMap: false │
│ - compression-webpack-plugin 预 gzip│
└─────────────────────────────────────┘
|
Why 三层都要做:单做某一层效果有限。代码层减体积(KB→KB)、服务器层减传输(KB→10%×KB)、框架层减请求数(多个 chunk 复用连接)。
2. 代码层面:CSS 位置与请求数
2.1 样式表放 head
1
2
3
4
5
6
7
8
9
10
11
12
| <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<!-- ✅ 把 CSS 放 head,避免 FOUC(无样式内容闪烁) -->
<link rel="stylesheet" href="example.css">
</head>
<body>
</body>
</html>
|
Why:浏览器渲染流水线是"解析 HTML → 遇到 CSS 阻塞渲染"。把 CSS 放最前面,能让浏览器一拿到 CSS 就开始构建 CSSOM,尽早完成首次绘制。
2.2 脚本放 body 底部
1
2
3
4
5
6
| <body>
<!-- 页面内容 -->
<!-- ✅ 脚本放 body 底部,不阻塞页面渲染和图片下载 -->
<script src="example.js"></script>
</body>
|
Why:<script> 标签默认阻塞——浏览器遇到 <script> 会停下后续的 HTML 解析,去下载并执行脚本。把脚本放底部(或加 defer / async)能避免这个阻塞。
2.3 使用外部 JS/CSS
1
2
3
4
5
6
7
8
9
10
11
| <!-- ✅ 推荐:外部文件,可被浏览器缓存 -->
<link rel="stylesheet" href="example.css">
<script src="example.js"></script>
<!-- ❌ 不推荐:内联到 HTML 里,每次都重新下载 -->
<style>
/* code */
</style>
<script>
/* code */
</script>
|
例外:极小的关键 CSS(如首屏字体样式)可内联——critical CSS 提取是另一项优化技术。
3. 代码层面:减少 HTTP 请求
Why 重要:HTTP/1.1 时代浏览器对同一域名的并发请求数有限制(Chrome 6 个)。每个请求都有 TCP 握手、TLS 握手、HTTP 头开销。请求越少越好。
3.1 雪碧图(CSS Sprites)
把多个小图标合到一张 PNG,用 background-position 定位:
1
2
3
| .icon-home { background: url(sprite.png) 0 0; }
.icon-user { background: url(sprite.png) -30px 0; }
.icon-cart { background: url(sprite.png) -60px 0; }
|
现代替代:SVG sprite / iconfont(Iconfont.cn / iconify)。SVG 可缩放、可改色、HTTP 请求少。
3.2 内联小图(base64)
1
| <img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/..." />
|
适用:小于 10KB 的图片(icon、二维码、小 logo)
不适用:大图(base64 比原图大 33%)
3.3 合并脚本与样式
1
2
3
4
5
6
7
8
9
| 拆得太细:
- /js/jquery.js
- /js/bootstrap.js
- /js/app.js
- /js/utils.js
合并后:
- /js/vendors.min.js
- /js/app.min.js
|
Webpack 自动化:mode: production 时 optimization.concatenateModules 自动合并。
4. 代码层面:CDN 与外部资源
4.1 为什么要用 CDN
CDN(Content Delivery Network)——把静态资源分发到全球各地的边缘服务器。用户访问时从最近的边缘节点拉资源,降低 RTT(往返时延)。
1
2
3
| <!-- 从 CDN 引入 Vue / React -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
4.2 公共 CDN 服务
| 服务 | 特点 |
|---|
| jsDelivr | 全球 750+ 节点,自动 gzip/brotli,支持 npm/GitHub |
| unpkg | npm 包直链,简单 |
| BootCDN | 国内公司维护,国内访问快 |
| CDNJS | Cloudflare 维护 |
4.3 HTTP/2 优势
- 多路复用:一个 TCP 连接并行多个请求,告别"6 个并发限制"
- 头部压缩:HPACK 算法把重复 header 压缩 80%+
- 服务器推送:服务器主动把后续资源推给客户端(已弃用)
前置条件:必须配 HTTPS——HTTP/2 规范要求加密。
5. 代码层面:Gzip 压缩
5.1 为什么 gzip 能省 50%-70%
1
2
| 原始 JS: 300 KB
gzip 后: 90 KB (节省 70%)
|
原理:gzip 用 LZ77 + Huffman 编码,把重复的字符串(如变量名、关键字、空白)替换为指针。文本类资源(JS/CSS/HTML)压缩比 60-80%,图片/视频已压缩过再 gzip 收益小(不到 5%)。
5.2 浏览器支持
1
| Accept-Encoding: gzip, deflate, br
|
- gzip:所有浏览器支持
- br(Brotli):Chrome / Firefox / Safari,压缩比比 gzip 高 15-20%
- deflate:兼容老 IE,已基本被 gzip 替代
5.3 在 Node.js 中间件启用
1
2
3
| // Express
const compression = require('compression')
app.use(compression())
|
6. 服务器层面:Nginx gzip 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # 开启 gzip
gzip on;
gzip_static on; # 优先用预压缩的 .gz 文件
gzip_min_length 1k; # 大于 1K 才压缩
gzip_buffers 4 16k;
gzip_http_version 1.1; # 1.1 起步才支持
gzip_comp_level 2; # 1-9,越高越费 CPU
gzip_types
text/plain
application/javascript
application/x-javascript
text/javascript
text/css
application/xml
application/xml+rss;
gzip_vary on; # 配合 Vary 头,CDN 能分别缓存 gzip / br
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\."; # IE 6 及以下不支持
|
7. 服务器层面:HTTP/2 多路复用
1
2
3
4
5
6
7
8
9
10
| # 启用 HTTP/2(必须先配 HTTPS)
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/private.key;
# 其余配置...
}
|
What “多路复用” 感受:HTTP/1.1 时代,加载 10 个 JS 文件要排队等 6 个并发槽(剩余 4 个等前 6 个完);HTTP/2 时代,10 个 JS 文件可以同时下载——首屏时间可能从 3s 降到 1.5s。
8. 服务器层面:缓存策略
8.1 强缓存:Expires / Cache-Control
1
2
3
4
5
| # 给静态资源加 1 天强缓存
location ~.*\.(svg|woff|js|css)$ {
root /yourFilePath;
expires 1d;
}
|
响应头:
1
| expires: Thu, 30 May 2019 20:51:42 GMT
|
浏览器看到这个头,在到期前不再发请求,直接用本地缓存。
HTTP/1.1 推荐用 Cache-Control 替代 Expires(避免客户端/服务端时钟不同步):
1
2
3
| server {
add_header Cache-Control max-age=72000; # 20000 小时 ≈ 833 天
}
|
最强缓存(10 年):
1
2
3
| location ~.*\.(js|css)$ {
add_header Cache-Control "public, max-age=315360000";
}
|
8.2 协商缓存:ETag / If-None-Match
为什么需要协商缓存:强缓存过期后,浏览器必须问服务器"我缓存里这份还能用吗"——服务器返回 304 Not Modified(不返回 body)或 200 OK(新内容)。
1
2
3
4
5
6
| 请求:
If-None-Match: "abc123-etag"
响应(资源未变):
HTTP/1.1 304 Not Modified
ETag: "abc123-etag"
|
Vue CLI 默认行为:每个静态资源都生成 ETag,文件名带 contenthash,过期后再请求自动 304。
8.3 强缓存 vs 协商缓存
| 策略 | 优点 | 缺点 |
|---|
| 强缓存 | 零请求 | 资源更新延迟(必须等过期) |
| 协商缓存 | 实时更新 | 仍要发请求(带 If-None-Match) |
最佳实践:
- HTML 文件:不缓存或
no-cache(必须每次校验) - 带
contenthash 的 JS/CSS:强缓存 1 年 - 图片/字体:强缓存 30 天
- 接口数据:协商缓存或不缓存
9. Vue 实战:compression-webpack-plugin + externals + sourceMap
写作背景:vue-cli@3.4 + Vue 2.6.6。这套配置至今仍适用(Vue 3 改 Vite 后的 vite-plugin-compression 是同一思路)。
9.1 安装
1
| npm install compression-webpack-plugin --save-dev
|
9.2 vue.config.js 完整优化配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| const CompressionWebpackPlugin = require('compression-webpack-plugin')
const ENV = process.env.NODE_ENV || 'development'
module.exports = {
configureWebpack: config => {
if (ENV === 'production') {
// 1. 预 gzip 压缩
config.plugins.push(
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.(js|css|html)$/,
threshold: 10240, // 大于 10KB 才压
minRatio: 0.8
})
)
// 2. 外部 CDN 化(业务 bundle 不再包含 Vue / VueRouter / Axios)
config.externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'axios': 'axios'
}
}
},
// 3. 关闭生产环境的 sourceMap(节省几百 KB + 防源码泄露)
productionSourceMap: false
}
|
9.3 index.html 配合 externals
1
2
3
4
5
6
7
8
9
10
| <body>
<div id="app"></div>
<!-- 仅在生产环境引入 CDN,dev 时 webpack 自己注入 -->
<% if (NODE_ENV === 'production') { %>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<% } %>
</body>
|
注意:vue.config.js 里的 externals 键名要与 CDN 暴露的全局变量名大小写一致——'vue': 'Vue'(小写 vue 是 import 名,大写 Vue 是全局变量)。
9.4 为什么这样能把首屏从 10s 压到 2s
| 优化项 | 节省 |
|---|
| 预 gzip | 静态资源体积 -70% |
| externals | 业务 bundle 减半(~500KB → 250KB) |
| productionSourceMap: false | 节省 200-500KB 隐藏文件 |
| 拆 vendor(vue-cli 默认) | 利用浏览器强缓存 |
| 配合 nginx gzip + HTTP/2 | 传输再减 70% |
作者实测:个人站点首页从 10s 降到 2s(4G 网络 + Chrome 70)。
10. 首屏时间从 10s 到 2s 的真实案例
| 阶段 | 体积 | 首屏时间 | 优化项 |
|---|
| 初始版本 | 1.2MB | 10s | 无 |
| 加 gzip | 480KB | 6s | nginx gzip on |
| 加 CDN | 320KB | 4s | externals + bootcss |
| 加 HTTP/2 | 320KB | 3s | listen 443 ssl http2 |
| 强缓存 + 预 gzip | 320KB | 2s | 完整组合 |
压测工具:
- Chrome DevTools → Network 面板(看瀑布图、size、time)
- Lighthouse(自动打分,给出优化建议)
- WebPageTest.org(多地点、多浏览器测试)
curl -w '%{time_total}\n' -o /dev/null -s <URL>(CI 中压测接口)
小结
前端性能优化不是单点突破,是**“代码 + 服务器 + 框架"三层联动**。优先级:
- 先量后优:用 Lighthouse / WebPageTest 跑一次,定位瓶颈
- 后端优先:开 gzip + HTTP/2 + 强缓存,能秒减 70% 体积
- 代码层:拆 chunk + externals + 雪碧图
- 框架层:Vue 用 compression-webpack-plugin,React 用
vite-plugin-compression - 持续监控:接 Sentry / 自建 RUM,监控真实用户的 FCP、LCP
下一步:本文专注"前端资源加载"层。Web Vitals 时代要看 LCP(Largest Contentful Paint)、CLS(Cumulative Layout Shift)、INP(Interaction to Next Paint)三大指标——单看"首屏时间"已经不够。
参考资料