12、WebKit 的网络栈:网络请求流程、HTTP/2 与 QUIC 支持、缓存策略(Memory Cache/Disk Cache)、预加载与预连接

聊到 WebKit 的网络栈,我脑子里第一个蹦出来的词就是「管道」。你想想看,用户在地址栏敲个网址,到页面渲染出来,中间这趟旅程,网络栈就是那条最关键的运输管道。今天咱们就把这条管道拆开,看看里面到底是怎么跑的。

网络请求的完整流程

一个网络请求从发起到结束,在 WebKit 里大致要经过这么几个阶段:

  1. URL 解析与协议匹配 —— 判断是 http、https 还是 file 协议
  2. DNS 查询 —— 把域名转成 IP 地址
  3. 建立连接 —— TCP 三次握手,TLS 握手(如果是 HTTPS)
  4. 发送请求 —— 构造 HTTP 请求头,发送给服务器
  5. 接收响应 —— 解析 HTTP 响应头,读取响应体
  6. 资源缓存决策 —— 判断要不要缓存、怎么缓存
  7. 数据交付 —— 把数据交给渲染引擎

嗯,这里要注意,WebKit 本身并不直接做网络 I/O。它依赖 Chromium 的 net 模块,或者 iOS 上的 CFNetwork。WebKit 更像是一个调度中心,它告诉底层:「我要这个资源,你帮我拿一下。」

核心接口:WebKit 中负责网络请求的核心类是 ResourceLoaderResourceHandle。前者负责调度策略,后者负责实际的网络通信。

HTTP/2 与 QUIC 支持

我记得几年前刚接触 HTTP/2 时,最大的感受就是「多路复用」终于来了。以前 HTTP/1.1 的队头阻塞问题,真是让人头疼。一个请求卡住了,后面所有请求都得等着。

WebKit 对 HTTP/2 的支持,说白了就是利用 二进制分帧层,把多个请求交错发送。你不需要等第一个请求的响应回来,就能发第二个请求。这在加载大量小资源(比如图片、CSS 碎片)时,效果特别明显。

至于 QUIC,它是基于 UDP 的下一代协议。我曾在项目中遇到过弱网环境,TCP 的丢包重传机制导致页面加载慢得像蜗牛。换成 QUIC 后,因为它是 0-RTT 握手,加上更好的丢包恢复机制,加载速度提升了一大截。

特性 HTTP/1.1 HTTP/2 QUIC (HTTP/3)
传输层 TCP TCP UDP
多路复用 不支持 支持 支持
队头阻塞 严重 TCP 层仍有 基本消除
连接建立 2-RTT 1-RTT 0-RTT
WebKit 支持 全版本 iOS 9+ / macOS 10.11+ iOS 13+ / macOS 10.15+

避坑指南:我曾经调试过一个诡异的问题——HTTP/2 的服务器推送(Server Push)在 WebKit 上有时会重复请求。后来发现是缓存策略没配好,导致推送的资源被当作「新鲜资源」重新请求。解决方案是确保推送的资源设置了正确的 Cache-Control 头。

缓存策略:Memory Cache 与 Disk Cache

WebKit 的缓存分两层:Memory CacheDisk Cache。说白了,一个在内存里,一个在硬盘上。

Memory Cache 的特点是快,但容量小。它存的是当前页面正在使用的资源,或者刚刚加载完的资源。一旦页面关闭,或者内存紧张,这些缓存就会被清掉。

Disk Cache 则持久化到硬盘,容量大得多。它遵循 HTTP 缓存协议,比如 Cache-Control: max-age=3600 这样的指令。WebKit 会把这些资源序列化后存到磁盘上,下次启动浏览器时还能用。

我个人习惯在调试时用 chrome://inspect 或者 Safari 的 Web Inspector 查看缓存状态。你可以在 Network 面板里看到每个资源是从哪里加载的:

  • from memory cache —— 从内存缓存读取
  • from disk cache —— 从磁盘缓存读取
  • from service worker —— 从 Service Worker 读取
  • 空 —— 从网络加载

注意:WebKit 的 Disk Cache 不是简单的文件存储。它使用了一种叫 Simple Cache 的机制,把资源分成多个块存储。这样做的好处是读写效率高,但坏处是如果缓存文件损坏,整个缓存目录可能都需要重建。我曾经遇到过用户反馈「浏览器不加载新图片」,排查下来就是 Disk Cache 的索引文件损坏了。

预加载与预连接

预加载和预连接,说白了就是「提前动手」。用户还没点击,浏览器就已经开始干活了。

预加载(Preload) 是通过 <link rel="preload"> 告诉浏览器:「这个资源很重要,你提前加载吧。」WebKit 会把这个请求的优先级提到最高,尽早发起网络请求。

预连接(Preconnect) 则是通过 <link rel="preconnect"> 告诉浏览器:「你提前跟这个服务器建立连接吧。」这样当真正需要请求资源时,DNS 查询、TCP 握手、TLS 握手都已经完成了,直接发送请求就行。

我建议在关键路径上使用预加载,比如首屏的字体文件、关键的 CSS 或 JS。预连接则适合那些你确定会用到、但不确定具体什么时候用的第三方域名,比如 CDN 或者分析服务。

代码示例:

<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/MyFont.woff2" as="font" type="font/woff2" crossorigin>

<!-- 预连接第三方 CDN -->
<link rel="preconnect" href="https://cdn.example.com">

<!-- 预加载 CSS,但非阻塞 -->
<link rel="preload" href="/styles/critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

知识体系总览

下面这张图把 WebKit 网络栈的核心模块串起来了。你可以看到从用户输入 URL 开始,到最终渲染的完整链路。

WebKit 网络栈核心流程 用户输入 URL URL 解析与协议匹配 DNS 查询 → TCP 握手 → TLS 握手 (HTTP/2 或 QUIC 协议协商) 发送请求 → 接收响应 → 缓存决策 Memory Cache / Disk Cache / 网络 数据交付给渲染引擎 预加载 / 预连接 HTTP/2 多路复用 虚线表示可选或增强功能,实线表示必经路径

这张图里,实线是必经路径,虚线是可选增强。预加载和预连接就像「提前铺路」,HTTP/2 和 QUIC 则是「优化运输方式」。缓存策略则是「仓库管理」,决定哪些货可以就近取。

好了,关于 WebKit 网络栈的核心内容就聊到这儿。下次你在调试页面加载慢的问题时,不妨从这几个角度去排查:是连接建立慢了?还是缓存没命中?或者是协议版本太旧?


公众号:蓝海资料掘金营,微信deep3321