---
Web 性能优化最佳实践
---

#### 目录

1. 前言
2. 在客户端缓存资源
3. 压缩传输的数据
4. 消除不必要的请求字节
5. 并行处理请求和响应
6. 针对 HTTP/1.1 的优化建议
7. 针对 HTTP/2.0 的优化建议

#### 前言

无论什么网络，也不管所用网络协议是什么版本，所有应用都应该致力于消除或减少不必要的网络延迟，将需要传输的数据压缩至最少。这两条标准是经典性能优化最佳实践，是其他数十条性能准则的出发点：

1. 减少 DNS 查找

   每一次主机名解析都需要一次网络往返，从而增加请求的延迟时间，同时还会阻塞后续请求。

2. 重用 TCP 连接

   尽可能使用持久连接，以消除 TCP 握手和慢启动延迟。

3. 减少 HTTP 重定向

   HTTP 重定向极费时间，特别是不同域名之间的重定向，更加费时；这里面既有额外的 DNS 查询、TCP 握手，还有其他延迟。最佳的重定向次数为零。

4. 使用 CDN（内容分发网络）

   把数据放到离用户地理位置更近的地方，可以显著减少每次 TCP 连接的网络延迟，增大吞吐量。

5. 去掉不必要的资源

6. 在客户端缓存资源

   应该缓存应用资源，从而避免每次请求都发送相同的内容。

7. 传输压缩过的内容

   传输前应该压缩应用资源，把要传输的字节减至最少。

8. 消除不必要的请求开销

   减少请求的 HTTP 首部数据（比如 HTTP Cookie），节省的时间相当于几次往返的延迟时间。

9. 并行处理请求和响应

   请求和响应的排队都会导致延迟，无论是客户端还是服务端。

10. 针对协议版本采取优化措施

    HTTP/1.x 支持有限的并行机制，要求打包资源、跨域分散资源，等等。相对而言，HTTP/2.0 只要建立一个连接就能实现最优性能，同时无需针对 HTTP/1.x 的那些优化方法。

#### 在客户端缓存资源

要说最快的网络请求，那就是不用发送请求就能获取资源。将之前下载过的数据缓存并维护好，就可以做到这一点。对于通过 HTTP 传输的资源，要保证首部包含适当的缓存字段：

- Cache-Control 首部用于制定缓存时间
- Last-Modified 和 ETag 首部提供验证机制

只要可能，就给每种资源都指定一个明确的缓存时间。这样客户端就可以直接使用本地副本，而不必每次都请求相同的内容。类似的，指定验证机制可以让客户端检查过期的资源是否有更新，没有更新，就没必要重新发送。

最后，还要注意应同时指定缓存时间和验证方法。

#### 压缩传输的数据

利用本地缓存可以让客户端避免每次请求都重复取得数据。不过，还是有一些资源是必须取得的，比如原来的资源过期了，或者有新资源，再或者资源不能缓存。对于这些资源，应该保证传输的字节数最少。因此要保证对它们进行最有效的压缩。

HTTP、CSS 和 JavaScript 等文本资源的大小经过 gzip 压缩平均可以减少 60～80%。而图片则需要仔细考量，比如选择最优的图片格式。

这里说一下 WebP 格式，它是 Google 开发的一种新的图片格式，这种格式的无损压缩和有损压缩效能都有所提升：

- WebP 的无损压缩图片比 PNG 的小 26%
- WebP 的有损压缩图片比 JPG 的小 25%~34%
- WebP 支持无损透明压缩，但因此仅增加了 22% 的字节

但是，这种格式需要客户端 CPU 多花点时间解码（大约相当于处理 JPG 的 1.4 倍），但字节的节省完全可以补偿处理时间的增长。

#### 消除不必要的请求字节

HTTP 是一种无状态协议，也就是说服务器不必保存每次请求的客户端的信息。然而，很多应用又依赖于状态信息以实现会话管理、个性化、分析等功能。为了实现这些功能，HTTP 做了扩展，允许任何网站针对自身来源关联和更新 Cookie 元数据：浏览器保存数据，而在随后发送给来源的每一个请求的 Cookie 首部中自动附加这些信息。

上述标准并未规定 Cookie 最大不能超过多大，但实践中大多数浏览器都将其限制为 4KB。与此同时，该标准还规定每个站点针对其来源可以有多个关联的 Cookie。于是，一个来源的 Cookie 就有可能多达几十 KB。不用说，这么多元数据随请求传递，必然会给应用带来明显的性能损失：

- 浏览器会在每个请求中自动附加关联的 Cookie 数据
- 在 HTTP/1.x 中，包括 Cookie 在内的所有 HTTP 首部都会在不压缩的状态下传输
- 在 HTTP/2.0 中，这些元数据经过压缩了，但开销依然不小
- 最坏的情况下，过大的 HTTP Cookie 会超过初始的 TCP 拥塞窗口，从而导致多余的网络往返

应该认真对待和监控 Cookie 的大小，确保只传输最低数量的元数据，比如安全会话令牌。同时，还应该利用服务器上共享的会话缓存，从中查询缓存的元数据。更好的结果，则是完全不用 Cookie，比如在请求图片、脚本和样式表等静态资源时，浏览器绝大多数情况下不必传输特定于客户端的元数据。

#### 并行处理请求和响应

为了让应用响应速度达到最快，应该尽可能第一时间就分派所有资源请求。可是，还有一点也要考虑，那就是所有这些请求以及它们对应的响应，将会被服务器如何处理。如果我们的请求在服务器上按先来后到的顺序依次排队，那就又会导致不必要的延迟。要想实现最佳性能，就要记住以下几点：

- 使用持久连接，从 HTTP/1.0 升级到 HTTP/1.1
- 利用多个 HTTP/1.1 连接实现并行下载
- 可能的情况下利用 HTTP/1.1 管道
- 考虑升级到 HTTP/2.0 以提升性能
- 确保服务器有足够的资源并行处理请求

如果不使用持久连接，则每个 HTTP 请求都要建立一个 TCP 连接。由于 TCP 握手和慢启动，多个 TCP 会造成明显的延迟。在使用 HTTP/1.1 的情况下，最好尽可能重用已有连接。如果碰上能使用 HTTP 管道的机会，不要放过。更好的选择，则是升级到 HTTP/2.0，从而获得最佳性能。

#### 针对 HTTP/1.1 的优化建议

针对 HTTP/1.x 的优化次序很重要：首先要配置服务器以最大限度的保证 TCP 和 TLS 的性能最优，然后在谨慎的选择和采用经典的应用最佳实践，之后在度量、迭代。

1. 利用 HTTP 管道

   如果你的应用可以控制客户端和服务器这两端，那么使用管道可以显著减少网络延迟。

2. 采用域名分区

   如果你的应用性能受限于默认的每来源 6 个连接，可以考虑将资源分散到多了来源。

3. 打包资源以减少 HTTP 请求

   拼接和精灵图等技巧有助于降低协议开销，又能达成类似管道的性能提升。

4. 嵌入小资源

   考虑直接在父文档中嵌入小资源，从而减少请求数量。

管道缺乏支持，而其他优化手段又各有各的利弊。事实上，这些优化措施如果使用不当，反倒会伤害性能。总之，要有务实的态度，通过度量来评估各种措施对性能的影响。

#### 针对 HTTP/2.0 的优化建议

HTTP 2.0 的主要目标就是提升传输性能，实现客户端与服务器间较低的延迟和较高的吞吐量。显然，在 TCP 和 TLS 之上实现最佳性能，同时消除不必要的网络延迟，从来没有如此重要过。

接下来，或许有点意外，那就是采用移动及其他经典的最佳做法：少发数据、削减请求，根据无线网络情况调整资源供给。不管是用什么版本的协议，减少传输的数据量和消除不必要的网络延迟，对任何应用都是最有效的优化手段。

最后，杜绝和忘记域名分区、文件拼接、图片精灵等不良的习惯，这些做法在 HTTP/2.0 之上完全没有必要。事实上，继续使用这些手段反而有害，可以利用 HTTP/2.0 内置的多路分发以及服务器推送等新功能。