Featured image of post 高性能前端:HTTP 缓存 / CDN / gzip 与 Vue 首屏优化实战

高性能前端:HTTP 缓存 / CDN / gzip 与 Vue 首屏优化实战

前端性能优化分三层:代码层面(CSS 位置、HTTP 请求合并、CDN)、服务器层面(gzip、HTTP/2、Expires、ETag)、Vue 专项(compression-webpack-plugin、externals、productionSourceMap)。一个真实案例把首屏从 10s 降到 2s。

为什么写这篇:2017 年 HTTP/2 全面铺开、vue-cli@3.4 默认开了很多性能优化(splitChunks、tree-shaking、terser)。本文把"代码 + 服务器 + 框架"三层优化串起来——一个真实项目把首屏从 10s 压到 2s。

适用读者:Vue 2/3 项目的性能优化负责人;想系统理解 HTTP 缓存 / CDN / gzip 的初中级前端。

前置知识:会用 vue-cli 启项目;知道 Nginx 配置文件长什么样。

目录

  1. 前端性能优化的三层模型
  2. 代码层面:CSS 位置与请求数
  3. 代码层面:减少 HTTP 请求
  4. 代码层面:CDN 与外部资源
  5. 代码层面:Gzip 压缩
  6. 服务器层面:Nginx gzip 配置
  7. 服务器层面:HTTP/2 多路复用
  8. 服务器层面:缓存策略
  9. Vue 实战:compression-webpack-plugin + externals + sourceMap
  10. 首屏时间从 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: productionoptimization.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
unpkgnpm 包直链,简单
BootCDN国内公司维护,国内访问快
CDNJSCloudflare 维护

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.2MB10s
加 gzip480KB6snginx gzip on
加 CDN320KB4sexternals + bootcss
加 HTTP/2320KB3slisten 443 ssl http2
强缓存 + 预 gzip320KB2s完整组合

压测工具

  • Chrome DevTools → Network 面板(看瀑布图、size、time)
  • Lighthouse(自动打分,给出优化建议)
  • WebPageTest.org(多地点、多浏览器测试)
  • curl -w '%{time_total}\n' -o /dev/null -s <URL>(CI 中压测接口)

小结

前端性能优化不是单点突破,是**“代码 + 服务器 + 框架"三层联动**。优先级:

  1. 先量后优:用 Lighthouse / WebPageTest 跑一次,定位瓶颈
  2. 后端优先:开 gzip + HTTP/2 + 强缓存,能秒减 70% 体积
  3. 代码层:拆 chunk + externals + 雪碧图
  4. 框架层:Vue 用 compression-webpack-plugin,React 用 vite-plugin-compression
  5. 持续监控:接 Sentry / 自建 RUM,监控真实用户的 FCP、LCP

下一步:本文专注"前端资源加载"层。Web Vitals 时代要看 LCP(Largest Contentful Paint)、CLS(Cumulative Layout Shift)、INP(Interaction to Next Paint)三大指标——单看"首屏时间"已经不够。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计