12、WebKit 的网络栈:网络请求流程、HTTP/2 与 QUIC 支持、缓存策略(Memory Cache/Disk Cache)、预加载与预连接
聊到 WebKit 的网络栈,我脑子里第一个蹦出来的词就是「管道」。你想想看,用户在地址栏敲个网址,到页面渲染出来,中间这趟旅程,网络栈就是那条最关键的运输管道。今天咱们就把这条管道拆开,看看里面到底是怎么跑的。
网络请求的完整流程
一个网络请求从发起到结束,在 WebKit 里大致要经过这么几个阶段:
- URL 解析与协议匹配 —— 判断是 http、https 还是 file 协议
- DNS 查询 —— 把域名转成 IP 地址
- 建立连接 —— TCP 三次握手,TLS 握手(如果是 HTTPS)
- 发送请求 —— 构造 HTTP 请求头,发送给服务器
- 接收响应 —— 解析 HTTP 响应头,读取响应体
- 资源缓存决策 —— 判断要不要缓存、怎么缓存
- 数据交付 —— 把数据交给渲染引擎
嗯,这里要注意,WebKit 本身并不直接做网络 I/O。它依赖 Chromium 的 net 模块,或者 iOS 上的 CFNetwork。WebKit 更像是一个调度中心,它告诉底层:「我要这个资源,你帮我拿一下。」
核心接口:WebKit 中负责网络请求的核心类是 ResourceLoader 和 ResourceHandle。前者负责调度策略,后者负责实际的网络通信。
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 Cache 和 Disk 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 开始,到最终渲染的完整链路。
这张图里,实线是必经路径,虚线是可选增强。预加载和预连接就像「提前铺路」,HTTP/2 和 QUIC 则是「优化运输方式」。缓存策略则是「仓库管理」,决定哪些货可以就近取。
好了,关于 WebKit 网络栈的核心内容就聊到这儿。下次你在调试页面加载慢的问题时,不妨从这几个角度去排查:是连接建立慢了?还是缓存没命中?或者是协议版本太旧?