Chromium 内核浏览器中不同类型的网络请求的执行是否为同一线程?


Chromium 内核浏览器中不同类型的网络请求的执行是否为同一线程?

背景

现代 web 前端框架中资源加载的方式通常分为两种,一种是声明式资源加载,另一种是程序式资源(XHR、Fetch 等)加载。XHR、Fetch 这种请求方式我们已经非常熟悉,另外一种其实我们也非常熟悉,比如使用 <img> 标签加载图片,使用 <link> 标签加载 CSS 文件。这种在 HTML 中使用标签加载资源的方式,我称之为声明式资源加载。

在我们的浏览器调试工具中我们可看到 XHR、Fetch 加载和图片加载等方式都有不同的筛选标签。看到这个调试面板,我不由想到这些不同类型的网络请求,在底层的执行机制上,尤其是线程模型上,是相同的吗?为了探索这个问题,于是便有的本文,本文将用作我的思考记录。

浏览器架构

要探讨这个问题我们首先需要回到浏览器本身的架构方式,必须首先理解现代浏览器赖以运行的宏观架构。我们都知道目前最强最先进的 Chromium 浏览器的架构方式是基于多进程模型的,它有三个主要的进程:

  • 浏览器进程(Browser Process): 这是整个浏览器的主控中心和协调者,也被称为“代理进程”(Broker Process)。它负责管理所有其他进程、绘制浏览器窗口的非网页部分(如地址栏、标签页、按钮)、维护用户配置、并执行核心的安全策略。它是权限最高的进程。

  • 渲染器进程(Renderer Process): 这些是处理网页内容的“工人”进程。得益于“站点隔离”(Site Isolation)特性,现代 Chrome 会为每个网站实例(包括跨站 iframe)分配一个独立的渲染器进程。每个渲染器进程都运行在严格的沙箱(Sandbox)环境中,其权限受到极大限制,无法直接访问用户的磁盘文件或直接与网络通信。它的核心职责是使用 Blink 渲染引擎解析 HTML、执行 JavaScript、计算布局并绘制页面。

  • 网络服务(Network Service): 作为 Chromium“服务化”(Servicification)架构重构的一部分,所有网络相关的代码被从特权较高的浏览器进程中剥离出来,迁移到一个独立的、同样被沙箱化的网络服务进程中。这一举措进一步强化了浏览器的安全壁垒。现在,沙箱化的渲染器进程唯一的网络访问途径,就是通过这个受到严格管控的网络服务。

对于一个网络请求的发起可以划分为两个阶段“请求的发起”和“请求的执行”。在这个架构下我们可以清晰的看到,请求过程中,渲染器进程负责“请求的发起”,而网络服务进程负责“请求的执行”。当渲染器进程中的 HTML 解析器遇到一个标签,或者 JavaScript 引擎执行一个 fetch 调用时,它并不能自己创建网络连接。相反,它必须通过 IPC(进程间通信)向浏览器进程发出请求,浏览器进程在进行安全检查后,再将请求转发给网络服务进程去执行。

当打开一个 tab 窗口时发生了什么

基于以上进程的流转,实际工作负载又被进一步细分到不同的线程上,以确保关键任务的响应性。当我们开一个新 tab 页时,Chromium 就会创建一个独立的进程来负责这个 tab 窗口的工作。在这个 tab 窗口的进程中,会有三个线程来负责主要工作:

  • 主线程/UI 线程(Main/UI Thread): 这是每个进程中最重要的线程。在浏览器进程中,它被称为 BrowserThread::UI,负责响应用户的所有界面操作,如点击、输入等。在渲染器进程中,它就是 Blink 的主线程,负责运行 HTML 解析器、执行几乎所有的 JavaScript 代码、进行样式计算、布局和绘制。这个线程的绝对天条是:永不阻塞。 任何耗时操作都会导致界面卡顿,严重影响用户体验。

  • IO 线程(IO Thread): 这是为处理异步输入/输出(I/O)而生的专用线程。在浏览器进程和网络服务进程中都存在一个 IO 线程(在浏览器进程中称为 BrowserThread::IO)。它的核心职责是处理所有的 IPC 消息和网络通信。整个网络堆栈被设计为主要运行在这个单一的、非阻塞的 IO 线程上,通过异步回调机制来处理网络事件。

  • 工作线程池(Worker Thread Pool): 这是一组通用的后台线程,用于执行那些可能阻塞或计算量大的任务,从而将它们从关键的 UI 线程和 IO 线程上卸载下来。

至此,我们可以对开头问题的核心——“不同网络请求是否使用相同线程”——给出一个基于架构的、更为精确的回答。<img>标签的解析和 fetch()的调用,都发生在渲染器进程的主线程上。这是它们生命周期的起点。然而,由于渲染器进程的沙箱限制,这些请求的实际网络 I/O 操作,都必须被代理到网络服务的 IO 线程上执行。这是它们生命周期的终点。因此,答案是:它们的发起线程相同,但执行线程也相同,只是发起和执行发生在不同的进程和线程上下文中。这种“发起与执行的分离”是现代浏览器为了兼顾安全与性能而做出的根本性架构决策。

结论

综上所述,一旦一个网络请求,无论它源自 HTML 解析器(声明式资源加载)还是 JavaScript API(程序式资源加载),跨越了进程边界抵达网络服务,它就会进入一个通用且高度标准化的处理流水线。

参考阅读