从输入URL到页面展示,这中间发生了什么?

76 阅读9分钟

这是一个非常经典且深度的问题。我将从浏览器多进程架构的底层视角,为你详细拆解从输入URL到页面展示的完整过程。这个过程涉及多个进程和线程的协同工作,我会明确每一步是由哪个进程主导,以及它们之间如何通信。

我们以 Chrome 为例,它的主要进程包括:

  1. 浏览器进程(Browser Process):只有一个,负责用户界面、地址栏、书签、前进/后退等,以及管理其他所有进程。它也是所有网络请求和文件访问的入口。
  2. 渲染进程(Renderer Process):通常每个标签页一个,负责标签页内发生的一切,包括 HTML/CSS/JS 的解析、渲染、合成等。出于安全考虑,它运行在沙箱中。
  3. 网络进程(Network Process):只有一个,负责处理所有网络请求,如下载资源。
  4. GPU 进程(GPU Process):只有一个,负责独立的 GPU 任务,如 UI 绘制和 3D CSS 效果。
  5. 插件进程(Plugin Process):为每种类型的插件(如 Flash)创建一个,同样运行在沙箱中。

第一阶段:用户输入与导航开始

主导进程:浏览器进程

  1. 处理输入

    • 当你在地址栏输入时,浏览器进程的 UI 线程 首先会判断你输入的是 搜索查询 还是一个 URL
    • 如果是搜索查询,会使用配置的搜索引擎合成带搜索词的 URL。
    • 如果是 URL,则会将其格式化(例如,为你补上 https://www 等)。
  2. 开始导航

    • 浏览器进程的 UI 线程 通过 进程间通信(IPC) 将 URL 请求发送给 网络进程
    • 此时,标签页的图标可能会变为加载状态,但标签页本身仍然显示之前页面的内容
  3. BeforeUnload 事件

    • 在发起新导航之前,浏览器进程会检查当前渲染的页面是否注册了 beforeunload 事件。如果有,会向当前页面的 渲染进程 发送 IPC 消息,让其执行该事件的回调函数。
    • 这个事件允许页面在离开前询问用户“确定要离开吗?”,可能会取消导航。如果没有注册,或用户确认离开,则导航继续。

第二阶段:发起网络请求

主导进程:网络进程

  1. 查找本地缓存

    • 网络进程接收到 URL 后,首先会检查本地缓存(如 HTTP 缓存Service Worker 缓存)。
    • 如果发现有效的缓存资源,并且没有过期,网络进程会直接将其返回给浏览器进程,后续流程将跳过。否则,进入网络请求流程。
  2. DNS 解析

    • 网络进程会检查域名是否在本地 hosts 文件或 DNS 缓存中。如果没有,会向 DNS 服务器发起请求,将域名解析为 IP 地址。
  3. 建立 TCP 连接

    • 拿到 IP 地址后,网络进程会通过系统套接字与目标服务器建立 TCP 连接。这个过程涉及经典的 三次握手
    • 如果使用的是 HTTPS 协议,在 TCP 连接建立后,还会进行 TLS 握手,以协商加密密钥,建立安全的通信通道。
  4. 发送 HTTP 请求

    • 连接建立后,网络进程会构建一个 HTTP 请求报文(包括请求行、请求头、请求体),并通过该连接发送给服务器。
  5. 处理 HTTP 响应

    • 服务器处理请求后,返回 HTTP 响应。网络进程开始接收响应数据。
    • 解析响应头:网络进程首先解析 HTTP 响应状态码和响应头。
      • 如果是 301/302 重定向,网络进程会通知浏览器进程,浏览器进程会用新的 URL 重新开始整个导航过程。
      • 如果是 200 OK,则继续处理。
    • 检查 Content-TypeContent-Type 响应头告诉浏览器数据的类型。如果是 text/html,浏览器会将其交给渲染进程处理。如果是 application/octet-stream,则视为下载资源,交给浏览器进程的下载管理器。

第三阶段:准备渲染进程

主导进程:浏览器进程 & 渲染进程

  1. 提交文档(Commit Navigation)

    • 一旦网络进程接收到足够的数据开始解析(通常是在收到响应头并确认是 HTML 后),它会通过 IPC 通知浏览器进程:“数据已准备好,请准备渲染进程”。
    • 浏览器进程会找到一个或创建一个 渲染进程 来承载这个页面。Chrome 通常为同一站点(相同的协议和根域名)的页面复用同一个渲染进程(Site Isolation 策略下会有例外)。
    • 浏览器进程通过 IPC 向渲染进程发送“提交导航”的消息,同时将响应数据流(数据流管道)转移给该渲染进程。
  2. 确认导航,更新界面

    • 渲染进程接收到“提交导航”的消息和持续的数据流后,会向浏览器进程发送一个 IPC 确认
    • 此时,导航被正式提交。浏览器进程会:
      • 更新地址栏的 URL。
      • 更新网页历史状态(History Object)。
      • 更新界面,如清除之前页面的 UI(比如显示一个空白页),并显示加载动画。

第四阶段:渲染进程解析与渲染

主导进程:渲染进程

这是最复杂的一步。渲染进程接收到的 HTML、CSS、JavaScript 字节数据,需要经过一系列步骤才能最终变成屏幕上的像素。渲染进程中的主线程(Main Thread)和合成线程(Compositor Thread)是这里的关键

  1. 构建 DOM 树

    • 渲染进程的主线程将接收到的 HTML 字节数据,通过一个 令牌化器(Tokenizer)解析器(Parser),根据 HTML 标准逐步构建出一棵 DOM(文档对象模型)树。这是一个表示页面结构的内存中的树状结构。
  2. 加载子资源与遇到 JavaScript

    • 在解析 HTML 的过程中,会遇到外部资源链接,如 <link rel="stylesheet"><img><script>
    • 对于 CSS 和图片,浏览器会预加载扫描(Preload Scanner) 这些资源,并同时向网络进程发起请求,这个过程是并行的。
    • 当解析到 <script> 标签(没有 asyncdefer 属性)时,DOM 构建会暂停!因为 JavaScript 可能会修改 DOM。主线程必须停止解析,下载(如果尚未缓存)并执行该 JavaScript 文件,执行完成后才继续构建 DOM。这是为什么建议将脚本放在底部或使用 async/defer 的原因。
  3. 构建 CSSOM

    • 主线程会解析 CSS(包括外部 CSS 文件、内联样式和样式表),计算出每个 DOM 节点的最终样式,并生成 CSSOM(CSS 对象模型)
  4. 布局(Layout)/ 重排(Reflow)

    • 主线程将 DOM 树和 CSSOM 合并,生成一棵 渲染树(Render Tree)。这棵树只包含可见的元素(不包含 display: none 的元素)。
    • 然后,主线程开始 布局 过程:计算渲染树中每个节点的几何信息(在视口内的确切位置和大小)。这个过程会输出一棵 布局树(Layout Tree)
  5. 绘制(Paint)

    • 有了布局树,主线程需要决定绘制的顺序。例如,z-index 会影响层叠顺序。主线程会遍历布局树,创建 绘制记录(Paint Records)。绘制记录是对绘制过程的注解,如“先画背景,再画文字,再画边框”。
    • 此时,主线程的工作基本完成。它生成了需要绘制的内容和顺序,但实际的 光栅化(Rasterization)显示(Display) 工作会交给其他线程。
  6. 合成(Compositing)

    • 为了优化性能,浏览器会将页面分解为多个图层(Layers)。主线程会遍历布局树来创建一棵图层树(Layer Tree)
    • 主线程将每个图层的绘制信息提交给 合成线程(Compositor Thread)
    • 合成线程 将每个图层分块(Tiling),并将每个图块发送给 光栅线程(Raster Threads)
    • 光栅线程 在 GPU 的帮助下,将每个图块光栅化——将它们转换为位图(像素点),并存储在 GPU 内存 中。
    • 当所有图块都光栅化完毕,合成线程会收集称为 “绘制四边形(Draw Quads)” 的信息(这些信息描述了图块在内存中的位置、在页面中的位置等),然后创建一个 “合成器帧(Compositor Frame)”。

第五阶段:最终显示

主导进程:浏览器进程 & GPU 进程

  1. 提交合成器帧

    • 合成线程通过 IPC 将合成器帧提交给 GPU 进程
  2. 绘制到屏幕

    • GPU 进程 将多个合成器帧(比如来自不同标签页的)最终合成一个图像,然后通过操作系统提供的 API(如 Windows 的 DWM, Linux 的 X Server, macOS 的 Quartz Compositor),将其绘制到屏幕上
  3. 加载完成

    • 当页面中的所有资源(包括图片、iframe 等)都加载完毕后,渲染进程的主线程会触发 DOMContentLoadedload 事件。
    • 渲染进程通过 IPC 通知浏览器进程,浏览器进程会停止标签页上的加载旋转图标,显示“已完成”状态。

总结与流程图

整个过程的进程间协作可以简化为以下流程图:

[用户输入 URL]
        |
        v
[浏览器进程 (UI线程)] -> 处理输入 -> 检查BeforeUnload
        |
        v (IPC)
[网络进程] -> 查找缓存 -> DNS解析 -> TCP/TLS握手 -> 发送HTTP请求 -> 处理响应
        |
        v (IPC)
[浏览器进程] -> 准备/分配渲染进程 -> 提交导航
        |
        v (IPC & 传输数据流)
[渲染进程] -> 构建DOM -> 加载子资源/执行JS -> 构建CSSOM -> 布局 -> 绘制 -> 合成
        | (主线程)                                        | (合成线程)
        |                                                 v (IPC)
        |                                            [GPU进程] -> 光栅化
        |                                                 |
        v (IPC)                                           v
[浏览器进程] <- 确认导航、更新UI                        [屏幕] <- 最终显示

这个过程体现了现代浏览器的核心设计思想:将复杂任务分解到独立的进程中,通过进程隔离提升稳定性、安全性和性能。每一个你感觉瞬间完成的导航背后,都上演了一场由多个进程精密配合的复杂交响乐。