Skip to content

HTTP 协议

HTTP 是浏览器与服务器通信的基础协议。渲染周期一文从浏览器内部视角梳理了页面加载流程,本文则聚焦于 HTTP 协议本身——版本演进的设计动机、缓存机制的工程实践,以及 HTTP/2 和 HTTP/3 带来的变化。

版本演进

HTTP 的版本迭代本质上是在解决两个问题:性能和可靠性。

HTTP/1.0 每个请求/响应都需要建立独立的 TCP 连接,请求完成后立即断开。在同一个页面需要加载数十个资源的场景下,频繁的 TCP 连接建立和断开造成了巨大的性能浪费。HTTP/1.1 引入了持久连接(Connection: keep-alive),允许在一个 TCP 连接上顺序发送多个请求,但这只是缓解而非解决问题——浏览器的并发连接数有限(Chrome 对同一域名限制 6 个),在连接数用满后后续请求只能排队等待,这就是队头阻塞(Head-of-Line Blocking)。HTTP/1.1 还引入了管道化(Pipelining)试图让请求并行发送,但由于设计上的缺陷(响应必须按请求顺序返回,中间某个响应慢了就会阻塞后续所有响应),浏览器厂商最终没有大规模实现这个特性。

HTTP/2 从根本上改变了请求的组织方式。它在同一个 TCP 连接上通过多路复用(Multiplexing)实现了真正的并行请求——所有请求和响应被拆分为二进制帧(Frame),在同一个连接上交错传输,服务端可以按任意顺序返回响应。此外,HTTP/2 使用 HPACK 算法压缩请求头,减少了大量重复头部字段(如 User-Agent、Cookie)的传输开销。但 HTTP/2 并没有完全解决队头阻塞问题——它解决了应用层的队头阻塞,但 TCP 层的队头阻塞仍然存在:当一个 TCP 包丢失时,所有后续数据的传输都会被阻塞直到重传完成。

HTTP/3 用 QUIC 协议替换了 TCP。QUIC 基于 UDP 实现,将传输层的可靠性和拥塞控制上移到应用层,彻底消除了 TCP 层的队头阻塞——每个 QUIC 流独立处理丢包重传,一个流的丢包不会影响其他流。QUIC 还将连接建立和 TLS 握手合并为一次往返(0-RTT 或 1-RTT),显著缩短了连接建立时间。更重要的是,QUIC 支持连接迁移——当用户从 Wi-Fi 切换到 4G 时,连接标识基于连接 ID 而非四元组(源 IP、源端口、目标 IP、目标端口),连接可以无缝迁移而不需要重新建立。

版本传输层并行方式队头阻塞
HTTP/1.1TCP有限并发连接应用层+传输层
HTTP/2TCP单连接多路复用传输层
HTTP/3QUIC/UDP单连接多路复用

请求方法与状态码

浏览器日常使用中最常见的 HTTP 方法是 GET 和 POST。GET 用于获取资源,POST 用于提交数据,这两个方法覆盖了绝大多数页面交互场景。HEAD 方法和 GET 相同但不返回响应体,工程中常用于检查资源是否存在或校验缓存。OPTIONS 方法主要用于 CORS 预检请求——浏览器在跨域发送非简单请求前,会先发送一个 OPTIONS 请求询问服务器是否允许。PUT、DELETE、PATCH 主要用于 RESTful API 场景,但在传统表单提交中浏览器不直接使用这些方法。

状态码的设计思路是让客户端无需解析响应体就能判断请求的处理结果。1xx 是信息性响应,日常开发中很少直接接触。2xx 表示成功,200 是最常见的成功响应。3xx 是重定向——301 是永久重定向,浏览器会缓存新的地址并自动跳转;302 是临时重定向,浏览器每次都会向原地址请求。304 Not Modified 在缓存协商中至关重要,表示资源未变化,浏览器直接使用本地缓存。4xx 是客户端错误,404 表示资源不存在,403 表示服务器拒绝访问,401 表示未认证。5xx 是服务端错误,500 是最通用的服务器内部错误。

在工程实践中,有几个状态码容易混淆:302 和 307 都是临时重定向,但 302 允许浏览器将 POST 方法改为 GET,而 307 保证方法和请求体不变;401 和 403 的区别是 401 表示未认证(需要登录),403 表示已认证但权限不足。

缓存机制

浏览器缓存是前端性能优化中投入产出比最高的手段之一。理解缓存机制的细节,能帮助你在部署静态资源时做出正确的缓存策略,避免用户看到过期内容或者不必要的网络请求。

浏览器查找缓存的优先级依次是:Service Worker 缓存 → 内存缓存 → 磁盘缓存 → Push 缓存。Service Worker 缓存由开发者通过 JavaScript 完全控制,可以实现任意缓存策略,包括离线访问。内存缓存存储在 RAM 中,速度最快但生命周期短(关闭标签页就清除),通常用于当前页面频繁使用的资源。磁盘缓存持久化存储,容量大但读取速度比内存慢。Push 缓存是 HTTP/2 服务器推送资源的临时缓存,生命周期很短,只在当前会话中有效。

强缓存与协商缓存

浏览器处理缓存分为两个阶段:先检查强缓存,如果命中就直接使用本地资源,不发送任何网络请求;如果未命中,再进入协商缓存阶段,向服务器验证资源是否变化。

强缓存通过响应头中的 Cache-Control 和 Expires 控制。Expires 是 HTTP/1.0 的产物,指定一个绝对的过期时间(GMT 格式),缺陷是依赖客户端本地时钟,如果用户修改了系统时间就会出问题。Cache-Control 是 HTTP/1.1 引入的替代方案,使用 max-age 指定相对过期时间(秒数),不依赖客户端时钟,优先级高于 Expires。

Cache-Control: max-age=31536000, immutable

immutable 标志告诉浏览器在 max-age 期间即使用户刷新页面也不要发送验证请求,这个标志配合内容哈希的文件名使用,可以完全消除已部署静态资源的重复请求。

协商缓存在强缓存过期后生效。浏览器向服务器发送带有验证信息的请求,服务器判断资源是否变化来决定返回 304 还是新资源。有两种验证方式:Last-Modified / If-Modified-Since 基于文件的最后修改时间,精度为秒级;ETag / If-None-Match 基于资源内容生成的唯一标识符,精度更高。ETag 的优先级高于 Last-Modified。ETag 分为强 ETag(基于内容的完整哈希)和弱 ETag(基于内容的部分特征,前缀加 W/),弱 ETag 适用于动态内容(如 HTML 模板中的时间戳),允许内容有微小变化但仍视为未修改。

// 首次请求,服务器返回
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT
Cache-Control: max-age=3600

// 强缓存过期后的再次请求,浏览器自动携带
If-None-Match: "abc123"
If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT

// 资源未变化,服务器返回
HTTP/1.1 304 Not Modified

304 响应只包含响应头不包含响应体,传输数据量很小,但仍然需要一次完整的网络往返。所以对于确定不会变化的静态资源(带内容哈希的 JS/CSS 文件),直接设置很长的 max-age 让强缓存覆盖整个资源生命周期,比依赖协商缓存更高效。

按资源类型制定缓存策略

实际工程中需要根据资源类型制定不同的缓存策略。HTML 文档通常使用 Cache-Control: no-cache,每次都走协商缓存验证是否有更新,因为 HTML 是入口文件,不缓存可以确保用户总能拿到最新版本。CSS 和 JavaScript 如果文件名包含内容哈希(如 app.a1b2c3.js),可以设置很长的缓存时间(max-age=31536000, immutable),文件内容变化时哈希值自然变化,URL 变了就不会命中旧缓存。图片和字体等静态资源同理,文件名带哈希 + 长期强缓存即可。API 响应通常根据数据更新频率设置较短的 max-age 配合 must-revalidate,确保过期后必须验证。

资源类型策略典型配置
HTML协商缓存no-cache
JS/CSS(带哈希)强缓存max-age=31536000, immutable
图片/字体强缓存max-age=31536000
API 响应短期缓存+验证max-age=60, must-revalidate

Cache-Control 的其他常用指令也值得了解:public 表示响应可以被中间代理(CDN)缓存,private 表示只有浏览器可以缓存;s-maxage 覆盖 max-age 专门用于共享缓存的过期时间,常用于 CDN 场景——CDN 缓存时间可以和浏览器缓存时间不同。Vary 头指定了哪些请求头字段的变化会导致不同的缓存版本,例如 Vary: Accept-Encoding 表示压缩和非压缩版本分别缓存。

HTTP/2 的工程影响

HTTP/2 的多路复用对前端工程实践有直接的影响。在 HTTP/1.1 时代,工程师需要通过域名分片(将静态资源分散到多个子域名来突破单域名的 6 个连接限制)、资源合并(将多个小文件合并为一个大文件以减少请求次数)、Sprite 图片等手段来优化加载性能。HTTP/2 的多路复用使这些技巧变得不必要甚至有害——域名分片反而会失去 HTTP/2 单连接复用的优势,资源合并则降低了缓存的粒度(合并后的一个大文件中任何一个部分变化都会导致整个文件缓存失效)。

HTTP/2 的头部压缩对 Cookie 密集型的应用有明显的优化效果。如果每个请求都携带大量的 Cookie,头部压缩可以将重复的 Cookie 字段压缩为极小的表示。同样,HTTP/2 的服务器推送(Server Push)允许服务器在客户端请求 HTML 时主动推送关联的 CSS 和 JavaScript,但由于浏览器对 Push 的缓存策略处理不佳(Push 的资源没有明确的缓存控制),实际工程中更倾向于用 <link rel="preload"> 来替代服务器推送。

Service Worker 与离线应用

Service Worker 是浏览器提供的一个可编程的网络代理层,运行在独立于页面的线程中,能够拦截和控制页面发出的所有网络请求。这使得开发者可以实现完全自定义的缓存策略,不受浏览器内置缓存行为的限制。

javascript
// Service Worker 中自定义缓存策略
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached
      return fetch(event.request).then(response => {
        const clone = response.clone()
        caches.open('v1').then(cache => cache.put(event.request, clone))
        return response
      })
    })
  )
})

常见的缓存策略有几种模式:Cache First(优先从缓存读取,缓存中没有再请求网络,适合不常变化的静态资源)、Network First(优先请求网络,网络失败再回退到缓存,适合频繁更新的数据)、Stale While Revalidate(先返回缓存,同时后台请求网络并更新缓存,适合允许短暂过期的资源)。选择哪种策略取决于具体的业务场景——一个新闻网站的 HTML 页面适合 Network First 或 Stale While Revalidate,而 CDN 上的静态图片则适合 Cache First。

配合 Cache API、IndexedDB 和 Web App Manifest,Service Worker 可以将一个普通网站升级为可安装的渐进式 Web 应用(PWA),支持离线访问、后台同步和推送通知。但在实际落地时需要注意 Service Worker 的更新策略——浏览器会在页面关闭后才会更新 Service Worker,这意味着如果用户一直不关闭页面,可能一直使用旧版本的缓存逻辑。