从输入 URL 到页面显示

本节目标

完整流程(面试与实战都高频):

① 输入 URL:浏览器判断是「搜索词」还是「网址」
② DNS 解析:域名 → IP(有缓存就跳过这步)
③ 建立 TCP 连接(三次握手)
④ TLS 握手(HTTPS,协商出对称密钥)
⑤ 发送 HTTP 请求(带 cookie、缓存相关头)
⑥ 服务器响应 → 检查缓存 → 解析 HTML
⑦ 渲染进程解析 HTML/CSS/JS → 布局绘制(下一章详讲)
⑧ 页面显示,其余资源继续加载

几个关键细节:

下面这段 Node 代码会真实发起一次请求,并分别打印 DNS 解析、TCP 握手、TLS 握手、首字节、下载的耗时,让你「看见」链路每一步:

// 运行环境:Node.js 14+(仅用内置 https,无需装包)
// 步骤:把下面内容存为 brw-l2.js,执行  node brw-l2.js
// 它会真实请求 https://example.com,用 socket 事件精确打印 DNS/TCP/TLS/首字节/下载 各阶段耗时(毫秒)

const { performance } = require('node:perf_hooks')
const https = require('node:https')

// 发起一次 HTTPS 请求,用 socket 的 lookup/connect/secureConnect 事件把「链路」精确分段计时
function measure(url) {
  return new Promise((resolve) => {
    const timings = {}
    timings.start = performance.now()                          // ① 开始

    const req = https.request(url, (res) => {
      timings.ttfb = performance.now()                          // ⑤ 收到首字节(TTFB)
      res.resume()
      res.on('end', () => { timings.end = performance.now(); resolve({ timings, status: res.statusCode }) })
    })

    // socket 事件能拿到 fetch 拿不到的三个细分阶段:
    req.on('socket', (socket) => {
      socket.on('lookup', () => { timings.dns = performance.now() })        // ② DNS 解析完成
      socket.on('connect', () => { timings.tcp = performance.now() })       // ③ TCP 三次握手完成
      socket.on('secureConnect', () => { timings.tls = performance.now() }) // ④ TLS 握手完成
    })
    req.on('error', (e) => resolve({ error: e.message }))
    req.end()
  })
}

;(async () => {
  const { timings, status, error } = await measure('https://example.com/')
  if (error) { console.error('请求失败:', error); process.exit(1) }
  const ms = (a, b) => (b - a).toFixed(1)
  console.log(`DNS 解析      : ${ms(timings.start, timings.dns)} ms`)
  console.log(`TCP 三次握手  : ${ms(timings.dns, timings.tcp)} ms`)
  console.log(`TLS 握手      : ${ms(timings.tcp, timings.tls)} ms`)
  console.log(`首字节(TTFB)  : ${ms(timings.tls, timings.ttfb)} ms(状态 ${status})`)
  console.log(`下载完成      : ${ms(timings.ttfb, timings.end)} ms`)
  console.log(`总耗时        : ${ms(timings.start, timings.end)} ms`)
})()

优化视角(对应后面「首屏性能」章节):

DNS 预解析(dns-prefetch)     → 提前把域名解析好,省去用户等待
HTTP/2 + CDN + 缓存           → 减少网络往返与传输耗时
关键 CSS/JS 内联或 defer       → 减少阻塞解析
压缩 + Tree Shaking           → 减少传输体积

名词解释

DNS(域名系统,Domain Name System):把人类好记的域名(example.com)翻译成机器用的 IP 地址(93.184.216.34)的分布式系统。它的「方法」可理解为:先查本地缓存(lookup 命中就返回)、未命中再向递归解析器查询。你可以把它类比成「电话簿」。

RTT(往返时延,Round Trip Time):数据从你这里到服务器再回到你这里花的时间。握手每多一个 RTT,用户就多等一份。方法/手段:用「连接复用(HTTP/2)」「0-RTT(HTTP/3/QUIC)」来减少 RTT 次数。

三次握手(Three-way Handshake):TCP 建立连接的三步确认(SYN → SYN+ACK → ACK),目的是双方都确认「我能发、你能收」。它不提供「方法调用」,而是一组状态变迁(CLOSED → SYN_SENT → ESTABLISHED)。

课后练习

练习 1:为什么第二次刷新同一个页面,往往比第一次快很多?请至少说出两个原因。

答案:① DNS 缓存:域名已解析过,浏览器/系统缓存了 IP,跳过 DNS 查询;② 网络/HTTP 缓存:静态资源命中强缓存(Cache-Control 未过期)直接读本地,或命中协商缓存拿到 304,不必重新下载。此外 TCP 连接也可能被复用(HTTP/2 多路复用)。

练习 2:把上面 brw-l2.js 里的 example.com 换成你常用的一个网站(如 https://www.baidu.com),运行后观察「建连+首字节」耗时。如果这一项特别高,最可能卡在哪?

答案:最可能卡在「网络往返(RTT)」或「服务器处理慢」。如果是跨地域/跨境访问,物理距离导致的 RTT 无法靠代码消除,只能靠 CDN(让用户就近访问边缘节点)来缓解;如果是服务器响应慢,那就是后端优化问题,前端爱莫能助。

总结

我觉得「输入 URL 到显示」这种题,背流程是最低级的学法,真正值钱的是建立**「耗时归因」的直觉**。当你看到页面慢,脑子里要能立刻拆出:是 DNS 慢?是握手 RTT 多?是首字节晚(后端慢)?还是下载体积大?每一种慢,解法完全不同——DNS 慢上 dns-prefetch,握手慢上 HTTP/2,体积大上压缩和分包。这一节你跑的那段 Node 代码,核心价值不是「发了个请求」,而是让你亲眼确认:慢,是慢在哪一个可测量的数字上。性能优化的全部艺术,就是「先测量、再归因、后对症」。