计算机 · 2026 年 11 月 11 日

从输入 URL 到页面出现:一次完整的请求之旅

地址栏回车之后,浏览器做了十几件事。用 Navigation Timing 把 DNS、TLS、TTFB 和渲染拆开,白屏三秒才能定位到具体哪一段。

在地址栏敲下回车,三秒后页面才出现。这三秒可能花在 DNS、TLS、服务端处理、下载 HTML,也可能 HTML 早就到了,卡在 CSS 和同步脚本上。不拆开看,优化就是碰运气。

浏览器也不是单进程。地址栏属于 browser 进程,网络请求交给 network service,页面解析和渲染在 renderer 进程。一次导航要在这三个进程之间走一圈,任何一环都能成为瓶颈。

从按键到发出请求#

browser 进程先判断输入是 URL 还是搜索词。是 URL 就做规范化:补 scheme、处理 IDNA(中文域名转 punycode)、查 HSTS 列表决定是否强制 https。

如果域名在 HSTS 预加载列表里,即使用户输入 http://,浏览器也会直接走 https,不会发出明文请求。这一步在 DevTools 的 Network 面板里看不到,但在 chrome://net-export 的日志里能看到 HSTS 决策记录。

然后是找连接:先看 socket 池里有没有到同源的可复用连接,没有才走 DNS。DNS 缓存有好几层——浏览器自身、操作系统 resolver,可能还有 DoH。任何一层命中,domainLookupEnd - domainLookupStart 都会接近 0,此时纠结 DNS 优化没有意义。

握手成本取决于物理距离#

TCP 三次握手一个 RTT,TLS 1.3 一次往返完成,TLS 1.2 要两次。同城机房 RTT 1ms 时这点开销可以忽略;跨洋 RTT 150ms 时,新建连接光握手就 300ms 以上,还没算 HTTP 请求本身。

这就是连接复用的价值。HTTP/2 在一条连接上多路复用,多个资源共享同一次握手;HTTP/3 基于 QUIC,把传输握手和 TLS 合并,还支持 0-RTT 恢复。performance.getEntriesByType('navigation')[0].nextHopProtocol 会告诉你实际用的是 h2、h3 还是 http/1.1。

重定向是另一个隐形开销。一次 302 意味着新的请求,若跨域还要重新 DNS 加握手。登陆后跳转、http 到 https、带不带尾斜杠的规范化,都可能在关键路径上叠两三个 RTT。nav.redirectCount 大于 0 时就该查。

第三方域名可以用 preconnect 提前完成 DNS、TCP 和 TLS,用 dns-prefetch 只做解析。但 preconnect 会占用连接槽,给七八个域都加上,收益可能变成负数。

首字节之后才是大头#

HTML 到达后,renderer 开始解析。这里有个常被忽略的机制:preload scanner 会在主解析器被阻塞时,提前扫描后面的 HTML,发现 <script src> 和 <link rel=stylesheet> 就去预加载。它不执行脚本,只负责尽早发起请求。

所以关键路径通常是:CSS 阻塞渲染,同步脚本阻塞解析。给脚本加 defer 会延后到解析完成、按顺序执行;async 是下载完就执行,顺序不保证。两者都不阻塞 HTML 解析,但 defer 脚本会在 DOMContentLoaded 之前跑完。

DOMContentLoaded 和 load 是两回事:前者是 DOM 就绪(defer 脚本执行完),后者要等图片、iframe 等所有资源。用 load 衡量白屏是错的,白屏对应的是首次绘制,也就是 FCP。

字体是常见的隐性阻塞点。自定义字体没加载完时,浏览器可能先不显示文字(FOIT),font-display: swap 能让它先用后备字体渲染,代价是可能闪一下。

还有一个顺序问题:CSS 里的 @import 是串行加载的,A 里的 @import B 意味着解析到 A 之后才发现 B。首屏关键样式直接内联,能省掉一次往返;首屏用不到的样式用 media 属性拆出去,别一股脑塞进主样式表。

服务端那段时间在做什么#

TTFB 是 requestStart 到 responseStart,中间包含服务端从内核 accept 到写回第一个字节的全过程。常见的排队点有四个:accept 队列、线程或进程池、数据库连接池、上游依赖。

判断方法是对比服务端自己的埋点。如果应用日志显示处理只花了 20ms,而 TTFB 是 800ms,那时间不在业务代码里,八成卡在入口排队或回程网络。ss -lnt 的 Recv-Q 持续非 0,说明 accept 队列有堆积,这时候调大 backlog 只是缓解,真正的问题在消费速度。

预加载不是银弹#

<link rel=preload> 只是告诉浏览器「这个资源马上要用,提前下载」,它不改变阻塞关系,也不会自动提升执行优先级。用错了反而抢走关键资源的带宽。

现代浏览器支持 fetchpriority="high",给 LCP 图片或首屏字体标上,比堆一堆 preload 更直接。判断标准很简单:被 preload 的资源必须在首屏渲染的关键路径上,否则只是提前占带宽。

二次访问为什么快#

回访时看 nav.type:正常导航是 navigate,从历史记录返回可能是 back_forward。bfcache 会把整页状态冻结在内存里,返回时几乎瞬间恢复;资源命中强缓存时 transferSize 为 0。

「用户说快、测试说慢」的争论,很多时候就出在这里:测试用的是无缓存的新会话,用户走的是 bfcache。

在页面控制台跑这段,直接得到各阶段耗时:

javascript
const nav = performance.getEntriesByType('navigation')[0];
const ms = (a, b) => Math.round(nav[b] - nav[a]);
console.table({
  DNS: ms('domainLookupStart', 'domainLookupEnd'),
  TCP: ms('connectStart', 'connectEnd'),
  // 复用连接时 secureConnectionStart 为 0,必须单独判断
  TLS: nav.secureConnectionStart ? Math.round(nav.connectEnd - nav.secureConnectionStart) : 0,
  TTFB: ms('requestStart', 'responseStart'),
  'HTML 下载': ms('responseStart', 'responseEnd'),
  'DOM 就绪': ms('responseEnd', 'domContentLoadedEventEnd'),
  '总耗时': Math.round(nav.duration),
});
console.log('协议:', nav.nextHopProtocol, '重定向:', nav.redirectCount);

new PerformanceObserver((list) => {
  for (const e of list.getEntries()) console.log('LCP', Math.round(e.startTime), e.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });

如果 TTFB 就有 2.5 秒,问题在服务端或网络,前端优化没意义。如果 TTFB 200ms 而 LCP 在 3 秒,就是资源加载或渲染阻塞。跨域资源的详细 timing 需要对方返回 Timing-Allow-Origin,否则很多字段是 0。

一次白屏的排查顺序#

线上反馈「打开很慢」时,我不会先开 Performance 面板,而是按固定顺序走:

  1. Network 面板按时间排序,看第一个请求的 TTFB。超过 800ms 就先查后端,前端先不动。
  2. 看 Waterfall 里有没有被阻塞的 CSS 或同步脚本,Content Download 段很长说明包太大。
  3. Console 里跑上面的脚本,确认 nextHopProtocol 和 LCP 对应的元素。
  4. 最后才看火焰图和长任务,此时已经知道该看哪一段。

这套顺序的价值是排除:每一步都可能直接结束排查,不用在面板里翻十分钟。顺手记一下 document.readyState,它只有 loading、interactive、complete 三个值,和 DOMContentLoaded、load 大致对应;写自动化脚本判断页面可用,用它比等固定秒数可靠。

按阶段对照优化手段#

阶段 指标 常见原因 手段
DNS domainLookup 解析慢、无缓存 预连接、减少域名、DoH
连接 connectStart→connectEnd 跨地域、无复用 HTTP/2、连接池、CDN
等待 TTFB 后端慢、排队 缓存、索引、流式输出
下载 responseEnd 包太大 压缩、分包、按需加载
渲染 FCP / LCP CSS 阻塞、大图、字体 内联关键 CSS、fetchpriority

结论#

  • 白屏先量 TTFB:它慢就是后端和网络,别在前端打转。
  • 复用连接能省一整个 RTT 以上,跨地域时尤其明显;确认 nextHopProtocol 是不是 h2。
  • DOMContentLoaded 不等于用户看到内容,用 FCP 和 LCP 衡量首屏。
  • 同步脚本和 CSS 是渲染阻塞的主要来源,defer 只解决前者。
  • 采集脚本要处理 secureConnectionStart 为 0 的情况,否则 TLS 耗时是错的。

完

#浏览器#HTTP#性能#前端
回到顶部 ↑