"从输入 URL 到页面展示,这中间发生了什么?"
这是一道计算机网络与浏览器原理的经典面试题。它看似基础,实则深不见底。对于初级开发者,可能只需要回答"DNS 解析、建立连接、下载文件、渲染页面"即可;但对于高级工程师而言,这道题考察的是对网络协议栈、浏览器多进程架构、渲染流水线以及性能优化的系统性理解。
本文将剥离表象,深入底层,以专业的视角还原这一过程的全貌。
一、 URL 解析与 DNS 查询
1. URL 结构拆解
URL(Uniform Resource Locator),统一资源定位符。浏览器首先会对用户输入的字符串进行解析。如果不符合 URL 规则,浏览器会将其视为搜索关键字传给默认搜索引擎;如果符合规则,则拆解为以下部分:
scheme://host.domain:port/path/filename?query#fragment
- Scheme: 协议类型(HTTP/HTTPS/FTP 等)。
- Host/Domain: 域名(如 juejin.cn)。
- Port: 端口号(HTTP 默认为 80,HTTPS 默认为 443)。
- Path: 资源路径。
- Query: 查询参数。
- Fragment: 锚点(注意:锚点不会被发送到服务器)。
2. DNS 解析流程
网络通讯是基于 TCP/IP 协议的,是通过 IP 地址而非域名进行定位。因此,浏览器的第一步是获取目标服务器的 IP 地址。
DNS 查询遵循级联缓存策略,查找顺序如下:
- 浏览器缓存: 浏览器会检查自身维护的 DNS 缓存。
- 系统缓存: 检查操作系统的 hosts 文件。
- 路由器缓存: 检查路由器的 DNS 记录。
- ISP DNS 缓存: 也就是本地 DNS 服务器(Local DNS),通常由网络服务提供商提供。
如果上述缓存均未命中,则发起递归查询 与迭代查询:
- 递归查询: 客户端向本地 DNS 服务器发起请求,如果本地 DNS 不知道,它会作为代理去替客户端查询。
- 迭代查询 : 本地 DNS 服务器依次向根域名服务器 、顶级域名服务器 、权威域名服务器发起请求,最终获取 IP 地址并返回给客户端。
进阶优化:
- DNS Prefetch: 现代前端通过 提前解析域名,减少后续请求的延迟。
- CDN 负载均衡: 在 DNS 解析阶段,智能 DNS 会根据用户的地理位置,返回距离用户最近的 CDN 节点 IP,而非源站 IP,从而实现内容分发加速。
二、 TCP 连接与 HTTP 请求
拿到 IP 地址后,浏览器与服务器建立连接。这是数据传输的基础。
1. TCP 三次握手
TCP(Transmission Control Protocol)提供可靠的传输服务。建立连接需要经过三次握手,确认双方的收发能力。
- 第一次握手(SYN) : 客户端发送 SYN=1, Seq=x。客户端进入 SYN_SEND 状态。此时证明客户端有发送能力。
- 第二次握手(SYN+ACK) : 服务端接收报文,回复 SYN=1, ACK=1, seq=y, ack=x+1。服务端进入 SYN_RCVD 状态。此时证明服务端有接收和发送能力。
- 第三次握手(ACK) : 客户端接收报文,回复 ACK=1, seq=x+1, ack=y+1。双方进入 ESTABLISHED 状态。此时证明客户端有接收能力。
核心问题:为什么是三次而不是两次?
主要是为了防止已失效的连接请求报文段又传送到了服务端,产生错误。如果只有两次握手,服务端收到失效的 SYN 包后误以为建立了新连接,会一直等待客户端发送数据,造成资源浪费。
2. TLS/SSL 握手(HTTPS)
如果是 HTTPS 协议,在 TCP 建立后,还需要进行 TLS 四次握手以协商加密密钥(Session Key)。过程包括交换支持的加密套件、验证服务器证书、通过非对称加密交换随机数等,最终生成对称加密密钥用于后续通信。
3. 发送 HTTP 请求
连接建立完毕,浏览器构建 HTTP 请求报文并发送。
- 请求行: 方法(GET/POST)、URL、协议版本。
- 请求头: User-Agent、Accept、Cookie 等。
- 请求体: POST 请求携带的数据。
服务器处理请求后,返回 HTTP 响应报文(状态行、响应头、响应体)。浏览器拿到响应体(通常是 HTML 文件),准备开始渲染。
三、 浏览器解析与渲染(核心重点)
这是前端工程师最需要关注的环节。现代浏览器采用多进程架构,主要包括Browser 进程 (主控)、网络进程 和渲染进程。
当网络进程下载完 HTML 数据后,会通过 IPC 通信将数据交给渲染进程(Renderer Process)。渲染主流程如下:
1. 解析 HTML 构建 DOM 树
浏览器无法直接理解 HTML 字符串,需要将其转化为对象模型(DOM)。
流程:Bytes(字节流) -> Characters(字符) -> Tokens(词法分析) -> Nodes(节点) -> DOM Tree。
注意:遇到
2. 解析 CSS 构建 CSSOM 树
浏览器下载 CSS 文件(.css)并解析为 CSSOM(CSS Object Model)。
关键点:
- CSS 下载不阻塞 DOM 树的解析。
- CSS 下载阻塞 Render Tree 的构建(因此会阻塞页面渲染)。
3. 生成渲染树(Render Tree)
DOM 树与 CSSOM 树结合,生成 Render Tree。
- 浏览器遍历 DOM 树的根节点,在 CSSOM 中找到对应的样式。
- 忽略不可见节点:display: none 的节点不会出现在 Render Tree 中(但 visibility: hidden 的节点会存在,因为它占据空间)。
- 去除元数据:head、script 等非视觉节点会被去除。
4. 布局(Layout / Reflow)
有了 Render Tree,浏览器已经知道有哪些节点以及样式,但还不知道它们的几何信息(位置、大小)。
布局阶段会从根节点递归计算每个元素在视口中的确切坐标和尺寸。这个过程在技术上被称为 Reflow(回流) 。
5. 绘制(Paint)
布局确定后,浏览器会生成绘制指令列表(如"在 x,y 处画一个红色矩形")。这个过程并不直接显示在屏幕上,而是生成图层(Layer)的绘制记录。
6. 合成(Composite)与显示
这是现代浏览器渲染优化的核心。
- 分层:浏览器会将页面分为不同的图层(Layer)。拥有 transform (3D)、will-change、position: fixed 等属性的元素会被提升为单独的合成层。
- 光栅化(Raster) :合成线程将图层切分为图块(Tile),并发送给 GPU 进行光栅化(生成位图)。
- 显示:一旦所有图块都被光栅化,浏览器会生成一个 DrawQuad 命令提交给 GPU 进程,最终将像素显示在屏幕上。
脚本阻塞与优化 :
为了避免 JS 阻塞 DOM 构建,可以使用 defer 和 async:
- defer: 异步下载,文档解析完成后、DOMContentLoaded 事件前按照顺序执行。
- async: 异步下载,下载完成后立即执行(可能打断 HTML 解析),执行顺序不固定。
四、 连接断开
当页面资源加载完毕,且不再需要通信时,通过 TCP 四次挥手 断开连接。
- 第一次挥手(FIN) : 主动方发送 FIN,进入 FIN_WAIT_1。
- 第二次挥手(ACK) : 被动方发送 ACK,进入 CLOSE_WAIT。主动方进入 FIN_WAIT_2。此时连接处于半关闭状态。
- 第三次挥手(FIN) : 被动方数据发送完毕,发送 FIN,进入 LAST_ACK。
- 第四次挥手(ACK) : 主动方发送 ACK,进入 TIME_WAIT。等待 2MSL(报文最大生存时间)后释放连接。
为什么需要 TIME_WAIT? 确保被动方收到了最后的 ACK。如果 ACK 丢失,被动方重传 FIN,主动方还能在 2MSL 内响应。
五、 面试高分指南(场景模拟)
场景:面试官问:"请详细描述从输入 URL 到页面展示发生了什么?"
回答策略范本:
1. 总述(宏观骨架)
"这个过程主要分为两个阶段:网络通信阶段 和页面渲染阶段。网络阶段负责将 URL 转换为 IP 并获取资源,渲染阶段负责将 HTML 代码转化为像素点。"
2. 网络通信阶段(突出细节)
- "首先是 DNS 解析 。浏览器会依次查询浏览器缓存、系统 hosts、路由器缓存,最后发起递归或迭代查询拿到 IP。这里可以提到 CDN 是如何通过 DNS 实现就近访问的。"
- "拿到 IP 后进行 TCP 三次握手 建立连接。如果是 HTTPS,还涉及 TLS 握手协商密钥。"
- "连接建立后发送 HTTP 请求。需要注意 HTTP/1.1 的 Keep-Alive 可以复用 TCP 连接,而 HTTP/2 更是通过多路复用解决了队头阻塞问题。"
3. 页面渲染阶段(展示深度)
- "浏览器解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM 树,两者合并生成 Render Tree。"
- "接着进行 Layout(回流) 计算位置大小,然后进行 Paint(重绘) 生成绘制指令。"
- "这里有一个关键点是 Composite(合成) 。现代浏览器会利用 GPU 加速,将 transform 或 opacity 的元素提升为独立图层。修改这些属性不会触发 Reflow 和 Repaint,只会触发 Composite,这是性能优化的核心。"
4. 脚本执行(补充)
- "在解析过程中,遇到 JS 会阻塞 DOM 构建。为了优化首屏,我们通常使用 defer 属性让脚本异步加载并在 HTML 解析完成后执行。"
总结
"整个流程结束于 TCP 四次挥手断开连接。这就构成了一个完整的浏览闭环。"