一、前言
页面在浏览器上的渲染并不是一个一条线的过程,而是多进程架构下协同的结果,本文从浏览器多进程的角度解构从url输入到页面渲染的整个流程的解析。
二、浏览器的多进程架构
1.进程与线程之间的关系
在操作系统中,进程是资源分配的最小单位,而线程是 CPU 调度的最小单位。
为了更好地理解浏览器架构,我们可以从以下三个维度来拆解它们的关系:
A. 包含与归属:工厂与工人
进程是容器:一个进程好比一个工厂车间,它拥有独立的内存空间、数据集和系统资源(如网络句柄、文件描述符)。
线程是执行者:线程是车间里的工人。一个进程可以包含多个线程,它们协同完成复杂的任务。
B. 共享与隔离:内存的边界
进程间相互隔离:为了保证系统的稳定性,进程与进程之间的内存是完全隔离的 。如果一个渲染进程(Tab页)因为代码崩溃了,它不会影响到浏览器主进程或其他 Tab 页。
线程间资源共享:同一个进程内的所有线程都可以访问该进程的内存空间 。这意味着 JS 引擎线程可以轻松地读取到由网络线程下载并存放在内存中的数据。
C. 协同与竞争:互斥锁的由来 ★★★
这是理解"为什么 JS 会阻塞渲染"的关键点:
在同一个渲染进程内,GUI 渲染线程和 JS 引擎线程是互斥的。
原因:因为 JavaScript 脚本具有修改 DOM 的能力。如果两者同时运行,GUI 线程正在绘制一个 DOM 节点,而 JS 线程同时把它删除了,就会导致渲染结果不可预期。因此,浏览器规定:当 JS 引擎工作时,GUI 渲染线程会被挂起。
2. 为什么浏览器要采用"多进程"而非"单进程多线程"?
在早期的浏览器中,所有功能都运行在一个进程里。现代浏览器演进为多进程架构,主要是为了解决三个核心痛点:
稳定性: 在单进程下,任何一个线程的崩溃(如 Flash 插件卡死或复杂的脚本死循环)都会导致整个浏览器瘫痪。多进程架构下,"一个 Tab 一个进程" 实现了故障隔离。
安全性: 浏览器通过沙箱 机制将渲染进程锁起来。由于渲染进程运行的是不受信任的第三方脚本,沙箱让它无法直接读写本地文件或调用系统 API。所有的敏感操作必须通过 IPC(进程间通信) 告知浏览器主进程,由主进程进行权限审查后代为执行。
流畅性: 多进程可以更充分地利用多核 CPU 的并行计算能力 。同时,某些耗时的操作(如网络下载、插件运行)被抽离到独立进程中,不会占用渲染进程的主线程。
3.浏览器的主要进程及其进程下的线程(4 个核心进程 和 2 个辅助/动态进程)
① 浏览器主进程
它是浏览器的核心,也是所有进程的父进程。
职责:
界面显示:负责地址栏、前进后退按钮、书签栏等浏览器"外壳"的 UI。
用户交互:监听鼠标点击、键盘输入。
进程管理:负责创建、销毁和协调其他子进程。
存储功能:负责管理 Cookie、本地存储(LocalStorage)等磁盘读写。
核心线程:
- UI 线程:负责绘制浏览器外壳(地址栏、书签栏、窗口控制按钮)。
- I/O 线程:负责与其他进程进行 IPC 通信。(进程管理)
它负责管理子进程。当你点击关闭按钮,是 UI 线程捕捉到信号,通知浏览器进程去销毁对应的渲染进程。
② 网络进程
原本是浏览器主进程中的一个线程,为了提升稳定性和安全性,现代 Chrome 将其独立为进程,专门负责处理与外部世界的资源交换。
职责:
资源加载:负责发起所有的网络请求 (HTTP/HTTPS),并接收服务器返回的数据。
协议解析:解析 HTTP 响应头、状态码,处理 301/302 重定向。
DNS 解析:将域名映射为 IP 地址。
缓存管理:根据 HTTP 头部信息(如 Cache-Control)判断并管理磁盘/内存中的网络资源缓存。
安全校验:拦截恶意 URL 访问(如 Safe Browsing 安全检查)。
Cookie 处理:负责 HTTP 响应中 Set-Cookie 的解析,以及请求时 Cookie 字段的自动注入。
核心线程:
网络协议栈线程:这是最忙碌的"工人",负责处理 TCP 握手、TLS 加密解密、以及 HTTP/1.1、H2、H3 协议的处理。
DNS 解析线程:专门负责寻找域名背后的 IP 地址,并维护 DNS 缓存。
Socket 管理线程:管理与不同服务器之间的长连接(Connection Pool)。
③ 渲染进程
这是前端代码真正运行的"车间",也是浏览器安全机制的核心。每个 Tab 标签页通常拥有一个独立的渲染进程,它运行在"沙箱"中,无法直接访问系统资源。
核心职责:
解析与构建:将 HTML、CSS 字节流转化为浏览器能理解的 DOM 树和 CSSOM 树。
脚本执行:运行 JavaScript 代码,处理用户交互逻辑。
布局与绘制:计算元素的大小位置,并生成最终的像素图像传给 GPU 进程。
五大核心线程:
A.GUI 渲染线程
职责:负责解析 HTML、CSS,构建 DOM 树、CSSOM 树、布局树和绘制。
特点:当界面需要重绘(Repaint)或由于某些操作引发回流(Reflow)时,该线程就会执行。
B.JS 引擎线程
职责:负责解析 JavaScript 脚本,运行代码。
核心痛点(互斥机制):JS 引擎线程与 GUI 渲染线程是互斥的。如果 JS 执行时间过长,就会导致页面渲染加载阻塞,出现掉帧或卡顿。
面试亮点:为什么互斥?因为 JS 拥有修改 DOM 的权限。如果两者并行,可能会出现"GUI 正在画背景,而 JS 删除了该节点"的竞态问题。
C.事件触发线程
职责:归属于浏览器而不是 JS 引擎,用来控制事件循环(Event Loop)。
工作机制:当事件被触发(如点击、AJAX 完成)时,该线程会将对应的回调任务加入到任务队列的末尾,等待 JS 引擎空闲时处理。
D.定时器触发线程
职责:负责 setTimeout 与 setInterval 的计时。
存在的意义:因为 JS 引擎是单线程的,如果处于阻塞状态就无法计时。因此需要独立线程计时,计时完毕后再通知事件触发线程将回调推入队列。
注意:W3C 标准规定,setTimeout 的间隔时间低于 4ms 会被自动设为 4ms。
E.异步 HTTP 请求线程
职责:在请求发起后,通过浏览器分配一个线程专门负责监控网络状态。
工作机制:当请求状态变更(如成功返回)时,如果设有回调函数,该线程就会通知"事件触发线程"将回调放入任务队列。
F. 合成线程 专门负责处理页面的分层和图像合成,不占用主线程(GUI渲染线程和JS引擎线程)。
职责:
接收指令:主线程完成布局和绘制列表后,将这些信息提交给合成线程。
图层切片:将页面图层划分为大小固定的图块(Tiles),优先栅格化视口内的内容。
调度栅格化:配合 GPU 进程将图块转换为位图(像素点)。
响应交互:直接处理页面的滚动 (Scroll) 和 缩放 (Zoom),而无需经过主线程。
核心优势(独立性):
非阻塞交互:由于合成线程与 JS 引擎线程、GUI 渲染线程不互斥。这意味着即使 JS 引擎正在运行一个死循环导致页面卡死,你依然可以流畅地滚动页面。
硬件加速:它是利用 GPU 资源的核心入口,通过处理 transform、opacity 等属性,实现无需重排重绘的高性能动画。
④ GPU 进程 (GPU Process)
最初仅用于处理 3D 图形,但随着现代网页对流畅度要求的提高,它已成为网页"排版合成"与"像素上色"的物理支柱。
职责:
硬件加速:将合成线程提交的图块由逻辑指令转换为显卡可识别的位图。
复合渲染:负责将来自不同进程(如浏览器进程的 UI、渲染进程的网页内容)的位图进行混合,最终绘制到显示器屏幕上。
核心线程:
GPU 渲染线程:与显卡驱动直接通信,执行真正的绘制操作。
为什么独立?
浏览器将 GPU 独立为进程,主要是因为图形处理涉及到复杂的操作系统底层调用和硬件驱动。驱动程序通常不如系统核心稳定,一旦 GPU 任务崩溃,浏览器只需重启该进程即可,而不会导致整个浏览器或所有标签页"黑屏"或死机。
⑤ 插件进程 (Plugin Process)
专门用于运行如 Flash、Silverlight 等第三方插件的进程。
职责:
隔离风险:插件往往由第三方开发,稳定性差且极易存在安全漏洞。
物理隔离:确保即便插件崩溃或被劫持,其破坏力也仅限在该进程内部,不会波及渲染进程(你的网页)或主进程。
随着 Chrome 彻底停止对 Flash 的支持,现代网页中插件进程已较少出现。注意不要将"插件"与"扩展"混淆。
⑥ 扩展进程 (Extension Process)
地位:为你安装的浏览器扩展程序(如 Vue Devtools, AdBlock, 翻译插件)提供独立的运行空间。
职责:
独立运行:确保扩展程序的 JS 逻辑不会占用网页渲染进程的 CPU 资源。
权限管控:浏览器进程会根据扩展申明的权限 ,严格控制扩展进程对网页内容(DOM)或系统 API 的访问。
思考题
渲染进程有GUI线程负责对html css js的解析和渲染,主进程的UI线程和gpu进程的gup加速线程也有类似功能,为什么要这样设计?
A. 渲染进程:逻辑计算的核心
虽然它叫"渲染进程",但它大部分时间在做逻辑转换。
GUI 线程的工作:它把代码字节流变成 DOM/CSSOM。最重要的是,它计算出 Layout(布局)。 它告诉浏览器:"这里有一个 100x100 的红色方块"。
它不直接画图:GUI 线程并不直接控制显示器像素,它生成的只是"绘制指令(Paint Records)"。
B. GPU 进程:硬件加速的真相
以前浏览器确实靠 CPU 画图(软件渲染),但 CPU 处理图形太慢了。
GPU 加速线程:它接收来自合成线程的指令 。因为 GPU 擅长并行处理大量像素,它把渲染进程算好的"图块"直接转为屏幕上的像素。
独立性:把 GPU 独立出来是为了防崩溃。图形驱动非常脆弱,如果 GPU 线程在渲染进程里,一个复杂的 3D 效果挂了,你的网页就崩了。
C. 主进程(UI 线程):窗口的守护者
为什么主进程也要参与"显示"?
非网页区域的渲染:网页之外的区域(地址栏、书签栏、前进后退按钮)不受渲染进程控制。
最终合成:这是一个关键点。屏幕上显示的内容 = 浏览器外壳位图 + 网页内容位图。
协作流程:GPU 进程会把画好的网页内容位图交给主进程,主进程把自己的 UI 位图叠上去,最后由主进程指挥显示器把这整张图显示出来。
一个具体的场景:改变 background-color
渲染进程 (JS/GUI 线程):JS 修改了 CSS,主线程重新计算样式,发现颜色变了,生成一份新的"绘制列表"。
渲染进程 (合成线程):拿到列表,把任务分块,发给 GPU 进程。
GPU 进程 (GPU 线程):调用显卡硬件,把受影响的像素点重新喷色,生成位图。
浏览器主进程 (UI 线程):把这张新的位图放在浏览器窗口的"白板"区域显示。
三、从url输入到页面渲染的全流程(结合浏览器多进程架构)
整个流程实质上是多个独立进程在浏览器主进程的调度下,通过 Mojo IPC(进程间通信) 进行的一场数据与控制权的接力。
1. 导航触发:浏览器主进程的调度与拦截
输入预处理:UI 线程 拦截地址栏输入。若为非 URL 字符串,调用搜索引擎封装 URL;若为合法 URL,则直接进入导航逻辑。
BeforeUnload 拦截:如果当前已存在页面,主进程通过 IPC 向当前渲染进程发出信号。渲染进程执行 JS 逻辑并返回结果。为了防止渲染进程无响应导致导航卡死,主进程会对这一过程设置 Timeout 阈值。
启动网络指令:UI 线程发起一个指向 网络进程 的 IPC 请求。
这里详细解释一下BeforeUnload和启动网络指令的过程及优化------
① BeforeUnload 拦截:给旧页面"交代遗言"的机会
当你点击一个链接或在地址栏回车时,当前的网页(旧页面)还没销毁。浏览器必须先询问它:"你还有没处理完的事吗?"
IPC 信号是什么? 浏览器主进程(管理窗口的)发现你要跳走了,它会发一个 IPC(进程间通信)消息 给当前网页所在的渲染进程。
渲染进程在做什么? 渲染进程接收信号后,会检查 JS 代码里有没有监听 beforeunload 事件。比如你在写博客,还没保存,JS 就会弹出一个对话框:"系统可能不会保存您所做的更改。确定要离开吗?"
为什么需要 Timeout(超时)阈值? 这是为了防死锁。如果旧页面的渲染进程崩了,或者 JS 写了个死循环(例如 while(true){}),它就无法回复主进程。如果没有超时机制,你的浏览器地址栏就会永远卡在那里。
底层逻辑: 主进程会启动一个定时器(比如几秒钟)。如果渲染进程在规定时间内没回话,主进程会认为这个渲染进程"挂了",直接强行掐断它的生命周期,强制开始加载新页面。
②启动网络指令:外交部正式出航
一旦旧页面被处理完(或者超时了),浏览器主进程就要去互联网上拿新页面的数据了。
UI 线程发起请求: 此时,主进程里的 UI 线程(负责处理地址栏、按钮点击的那个工人)会整理好目标 URL、Cookie、请求头等信息。
指向网络进程的 IPC 请求: 在现代 Chrome 中,主进程自己不负责下载。它会把刚才整理好的"请求包"通过 IPC 扔给 网络进程。
形象点说: 主进程(CEO)给网络进程(外交部)打了个电话:"喂,去帮我把 github.com 的 HTML 字节流取回来。"
③为什么这两步是"并行的优化点"?
这里有一个非常硬核的亮点:现代浏览器并不会等 beforeunload 彻底结束才去发起网络请求。
为了快,浏览器通常会采取 并行策略:
一边让主进程询问旧页面是否要离开。
一边同步通知网络进程去进行 DNS 解析 和 建立连接。
如果用户最后点击了"取消离开",浏览器就把刚发起的网络请求掐断。如果用户确定离开,此时网络连接可能已经建好了,网页秒开。这就是所谓的 "导航预加载"思想。
2. 网络资源获取:网络进程的"外交"与"初筛"
当网络进程接到主进程的指令后,它开始在互联网上为网页寻找材料。
A. 物理链路的打通(建立连接)
浏览器缓存:首先检查网络进程内存中存储的 DNS 记录(通常缓存 1 分钟)
DNS 与握手 :如果浏览器缓存未命中网络进程会去查 IP 地址(DNS),然后进行 TCP 三次握手。如果是 HTTPS,还要进行 TLS 加密握手。
亮点:为了快,浏览器会维护一个 连接池。如果最近刚访问过这个域名,它会直接复用之前的"管道",省去握手时间。
B. 响应头的解析与"重定向"黑箱
内部消化重定向 :如果服务器返回 301/302(重定向),网络进程不会跑回去告诉主进程,而是自己在内部重新发起新的请求。
逻辑意义:对主进程和渲染进程来说,它们只关心最终拿到的结果,中间转了几次弯(重定向),网络进程在底层偷偷帮你处理好了。
C. 核心:响应体的"首包"嗅探
这是全流程中最精妙的地方。当网络进程收到服务器返回的第一份数据包(通常是前几个字节)时:
确定身份:网络进程会查看 Content-Type。
如果是 text/html,它就知道:"正主来了,准备通知渲染进程干活"。
如果是 application/octet-stream,它会意识到:"这是一个下载任务",于是把请求转交给下载管理器,导航流程在此终止。
建立数据管道:
核心机制:一旦确认是 HTML,网络进程不会等整个网页下载完。
它会建立一条"数据长管"。管子的这头在网络进程(继续下载后续字节),管子的那头直接插进未来的渲染进程。
它的意义:实现"边下载边解析",极大地缩短了白屏时间。
3. 提交导航:控制权从主进程移交给渲染进程
这是导航阶段最核心的"状态切换"点,标志着页面正式从旧地址切换到新地址。
进程分配:主进程根据 Site Isolation 策略分配渲染进程。如果是同站跳转,可能复用原有进程;否则启动新进程。
Commit 指令:主进程发送 CommitNavigation 消息给目标 渲染进程。
数据交接:主进程会将网络进程中那个 Data Pipe 的句柄随指令发送给渲染进程。
确认反馈:渲染进程收到句柄后,直接从管道读取数据流。一旦开始解析,渲染进程向主进程发送 DidCommitProvisionalLoad。
状态切换:主进程收到反馈后,执行 UI 状态更新(更新地址栏、重置历史记录、刷新前进按钮)。此时旧页面正式被销毁。
4. 渲染流水线:渲染进程与 GPU 的像素产出
在渲染进程接收到"数据管道"的句柄后,内部的主线程、合成线程 与 GPU 进程 开始高度协同。
A. 解析与构建:将字节转化为结构
流式解析:主线程无需等待 HTML 下载完成。通过 Data Pipe,每接收到一个数据包,GUI 渲染线程就会立即启动解析,边下载边构建 DOM 树。
样式计算(CSSOM):主线程解析 CSS 样式,计算出每个 DOM 节点的最终样式。
亮点(互斥机制):此阶段若遇到 <script>,主线程会挂起 GUI 渲染线程,切换到 JS 引擎线程。这种互斥确保了 JS 在修改 DOM 时不会产生渲染竞态。
B. 几何计算:确定空间坐标
布局树(Layout Tree)构建:主线程将 DOM 与 CSSOM 合并。它会过滤掉 display: none 的节点,仅保留可见元素。
几何量算:主线程精确计算每个元素在三维空间中的 (x, y) 坐标、宽高及层级。
产物:一棵包含所有几何信息的布局树。
C. 记录与图层化:生成施工图纸
分层:为了处理 3D 转换(transform)或滚动,主线程会根据属性将页面拆分为多个图层。
绘制记录(Paint):主线程并不直接画图,而是将每个图层的绘制逻辑拆解为一个个指令(如:"在此处画正方形","在彼处填充红色")。
产物:一份名为 绘制记录(Paint Records) 的逻辑清单。
D. 栅格化与合成:像素的工业产出
任务此时从主线程移交给合成线程,进入真正的硬件加速阶段。
切片(Tiling):合成线程将巨大图层划分为固定大小的 图块(Tiles),优先处理视口(用户肉眼可见区域)内的内容。
栅格化:
合成线程通过 IPC 向 GPU 进程 发出指令。
GPU 进程 利用显卡的并行计算能力,将图块指令转化为显存中的位图。
复合与上屏:
合成线程收集所有图块位图,生成一份"指引(Compositor Frame)"。 浏览器主进程 接收该指引,将网页位图与浏览器外壳(地址栏等)进行叠加,最终由 GPU 刷新到屏幕上。
为什么要把"合成"独立出来?
非阻塞滚动:当主线程因为运行复杂的 JavaScript 而卡死时,合成线程 依然可以独立工作。它能直接利用 GPU 显存里已有的位图进行位移偏移,这就是为什么即便网页脚本卡顿,你依然能流畅滑动(Scroll)页面的原因。
硬件加速:通过 transform 或 opacity 做的动画,直接在合成阶段完成,不触发主线程的"重排"或"重绘",实现了真正的性能最优。
四、流水线视角的重排、重绘与合成
理解了多进程协作的渲染流水线后,我们就能从底层逻辑解释:为什么有的代码会让页面卡顿,而有的代码却能实现 60fps 的丝滑动画? 关键在于你的操作强迫流水线"回溯"到了哪一步。
1. 重排
触发原因:修改了影响几何空间的属性(如 width, height, margin, padding, border, display 等),或调整浏览器窗口大小。
流水线回溯:
主线程:必须重新经历 样式计算 -> 布局 -> 图层分层 -> 生成绘制列表 。
合成线程:重新进行图块划分。
GPU 进程:重新进行栅格化和位图上传。
这是开销最大的操作,因为它触发了全量流水线,且深度依赖主线程的计算压力。
2. 重绘
触发原因:修改了不影响布局、仅影响视觉外观的属性(如 color, background-color, visibility, box-shadow 等)。
流水线回溯:
主线程:跳过布局和分层,直接重新生成 绘制记录。
合成/GPU 进程:重新进行栅格化。
开销中等。虽然避开了几何几何计算,但依然需要主线程生成指令并触发 GPU 重新喷色。
3. 合成 (Composite):硬件加速的"超车道"
触发原因:使用 CSS 的 transform(位移、缩放、旋转)或 opacity。
流水线表现:
主线程完全不参与。
合成线程:直接接收指令,在 GPU 中利用已有的图块位图进行矩阵变换。
开销极低。这是多进程架构带来的最大红利------动画直接在合成线程与 GPU 进程间通讯,即使此时 JS 引擎在主线程里跑死循环,合成动画依然能流畅运行。
五、总结
通过对浏览器多进程架构及渲染流水线的深度解构,我们可以发现,从输入 URL 到页面呈现,本质上是一场多进程间的"接力赛"与流水线上的"精密加工"。
1. 核心链路回顾
我们可以将整个漫长的流程浓缩为四个关键的瞬间:
主进程:拦截输入,启动导航,指挥网络部出航。
网络进程:打通链路,嗅探内容,并建立指向未来的数据管道。
渲染进程-主线程:将字节流转化为 DOM/CSSOM,并在几何计算中确定每一个像素的坐标。
合成线程 & GPU:利用硬件加速,将逻辑指令转化为位图,实现最终的像素上屏。
2. 给前端开发的性能启示
理解了这套底层机制,我们对"性能优化"的理解便不再流于表面,而是进化为一种流水线思维:
保护主线程:GUI 渲染与 JS 执行的互斥性告诉我们,长任务是掉帧的元凶。我们应当利用 Web Workers 或时间切片来释放主线程。
善用合成器:优先使用 transform 和 opacity 实现动画,本质上是在利用多进程架构的红利,绕过拥挤的主线程,走 GPU 加速的"超车道"。
尊重重排规律:减少对 offsetWidth 等属性的频繁读取,实质上是在保护流水线不被"强制同步布局"打断,避免昂贵的重复计算。
3. 写在最后
浏览器作为现代最复杂的软件之一,其多进程架构是稳定性、安全性和高性能巴巴博弈后的终极方案。
作为开发者,理解底层是为了更自由地构建上层。 当你再次打开浏览器的 Performance 面板,看到那些交织的进程与线程曲线时,你看到的不再是枯燥的数据,而是一场由数万行 C++ 代码支撑、毫秒必争的协作交响乐。