
在开发中,我们常聚焦于代码的健壮性和执行效率,却较少从浏览器角度思考其代码执行方式。本文将着重讲解浏览器的执行机制,助力开发者优化代码,提升 Web 应用性能
前置文档
CPU VS GPU

- 中央处理器(Central Processing Unit) :通用处理器,负责管理和调度任务的高级打工人,从过去的单核心转变为目前的多核心,为手机和笔记本电脑提供强大的计算能力
- 图形处理单元(Graphics Processing Unit) :图形和图像处理工具,上千个核心,适合并行计算简单的任务,如:图形渲染、深度学习等
CPU 和 GPU 在计算机系统中是相互协作的,CPU 负责管理和调度任务,如:自己执行通用计算任务、图形渲染交给 GPU
进程 VS 线程

- 进程(Process): 操作系统进行资源分配和调度的基本单位,有独立的内存、数据、代码和运行环境,表明应用正在执行的程序。启动系统会创建进程,并提供内存块使用,后续状态保留在此私有内存空间中。关闭应用,进程消失、操作系统释放内存
- 线程(Thread) :进程中执行单元,操作系统调度最小单位,线程共享进程资源、独立的执行路径

bosombaby.blog.csdn.net/article/det...
- 想要更加深入的了解,可以参考我之前系的一篇关于微信小程序双线程架构的文章
浏览器架构

WEB 浏览器如何利用进程和线程进行构建,没有一套规范的标准,每一款浏览器的底层各不相同,本篇文章主要对 Chrome 当前最新的架构进行拆解


进程 | 控制内容 |
---|---|
浏览器进程 | 控制应用的"Chrome"部分(地址栏、书签、返回和前进按钮) 处理网络浏览器的不可见特权部分(界面、网络、存储、文件) |
渲染进程 | 控制显示网站的标签页中的所有内容 |
插件进程 | 控制网站使用的所有插件(如 Flash) |
GPU进程 | 隔离地处理 GPU 任务,不受其他进程影响 处理来自多个应用的请求,并在同一页面中绘制 |

- 在现代浏览器中,顶部浏览器进程负责处理其他进程的工作。同时Chrome 会尽可能的为每一个标签页分配一个渲染进程(包括 iframe),即使一个标签页出现问题,其他的不会受到影响 (安全性、沙盒化)
- 但这个可能会有性能问题,所以当设备内存上限,在同一网站多个标签页共用一个进程 (进程合并)
网站隔离

网站隔离本质为跨网站的 iframe 运行单独的渲染进程,保证数据隔离。通信方式如下:
- 同源:直接访问 DOM、共享 storage 存储、父页面转发
- 不同源:postMessage、代理服务中转
Chrome 启用网站隔离需要考虑跨地址通信方式、调试模式的改造,所以是一项需要多年努力开发的任务
导航流程
处理输入

位于浏览器进程的界面线程会先判断,搜索查询(定位搜索引擎)?网址访问(定向请求网站)?
发送请求

用户按钮 Enter 键,界面线程发起网络调用以获取网站内容,标签显示加载旋转图表,网络线程遵循对应的协议(DNS IP 查询、TLS 认证、TCP 握手/挥手 等)进行处理
对于服务器会返回重定向标头,网络线程会与界面线程通信,让他重新发起网址请求
类型判断

响应正文传入后,网络线程会查看数据流前几个字节,对于 Content-Type 标头缺失会进行 MIME 类型嗅探。静态资源文件会传给渲染程序进程,GET 下载请求,需要把数据传给下载管理器

系统也会在此时进行安全性检查,如果网站和响应数据和 Chrome 存储的恶意网站匹配,此时网络线程会发出提醒,显示网站不安全,是否要继续访问。此外,跨域拦截操作也会在这个步骤进行处理
渲染进程启动

完成上面所有判断检查,网络线程 => 界面线程 => 渲染进程,准备开始渲染网页,这部分渲染详情放下面了
由于网络请求 有延迟,所以说 在网络线程发送网址请求时知道要导航到哪个网站。此时 界面线程会提前通知渲染进程启动,让它处于待机状态。但是,如果服务器返回重定向了,则会启用新的渲染进程
渲染进程运行

当前阶段数据和渲染进程已就绪,浏览器进行会向渲染进程发送数据并传递数据流,已保证持续接受 HTML 数据。当浏览器进程接受到渲染进程的确认消息后,导航完成,文档加载阶段开始。
此时,地址栏的证书和加载状态更新,并存储历史记录、返回/前进页面的状态(存储在浏览器中)
切换网站导航

网站渲染结束和切换网站导航会触发对应的生命周期事件(这里简单介绍下,更为详细的页面生命周期后面有空会出一篇文章进行讲解):
onLoad(渲染完成) :渲染进程将加载结束的状态返回浏览器进程,浏览器接受并停止 loading 状态
beforeunload(卸载前) :页面切换前的处理事件,比如提醒用户是否关闭当前网页。注意不要添加空事件,以防止执行处理脚本才进行导航的延时。有两种方式进行触发,第一种 window.location 从渲染进程通知浏览器进程,第二种 浏览器 输入框直接输入重新导航渲染
javascript
// 渲染进程 (Renderer Process)
function onPageLoadComplete() {
// 1. DOM 解析完成
DOM.parseComplete();
// 2. 资源加载完成 (CSS, JS, 图片等)
Resources.loadComplete();
// 3. 页面渲染完成
Page.renderComplete();
// 4. 渲染进程向浏览器进程发送加载完成信号
IPC.sendMessage(BrowserProcess, {
type: 'PAGE_LOAD_COMPLETE',
status: 'SUCCESS',
timestamp: Date.now()
});
}
// 浏览器进程 (Browser Process)
function onReceiveLoadComplete(message) {
if (message.type === 'PAGE_LOAD_COMPLETE') {
// 1. 接收渲染进程的加载完成状态
LoadingStatus.receive(message.status);
// 2. 停止 loading 状态显示
UI.stopLoadingIndicator();
// 3. 更新地址栏状态
AddressBar.updateStatus('LOADED');
// 4. 触发 load 事件
Window.dispatchEvent('load');
}
}
javascript
// 方式一:window.location 触发(渲染进程通知浏览器进程)
function navigateByScript(newUrl) {
// 1. 渲染进程检测到导航请求
NavigationRequest.detect();
// 2. 触发 beforeunload 事件
Event.trigger('beforeunload', {
canCancel: true,
source: 'SCRIPT'
});
// 3. 如果有事件处理器且返回值不为空
if (beforeunloadHandler.exists() && beforeunloadHandler.returnValue) {
// 显示确认对话框
UserConfirmation.show(beforeunloadHandler.returnValue);
if (!UserConfirmation.confirmed) {
return; // 取消导航
}
}
// 4. 渲染进程向浏览器进程发送导航请求
IPC.sendMessage(BrowserProcess, {
type: 'NAVIGATION_REQUEST',
url: newUrl,
source: 'RENDERER'
});
}
// 方式二:浏览器地址栏直接输入
function navigateByAddressBar(newUrl) {
// 1. 浏览器进程检测到地址栏输入
AddressBar.onInputChange(newUrl);
// 2. 浏览器进程向渲染进程发送卸载通知
IPC.sendMessage(RendererProcess, {
type: 'PREPARE_UNLOAD',
newUrl: newUrl
});
// 3. 渲染进程触发 beforeunload 事件
Event.trigger('beforeunload', {
canCancel: true,
source: 'BROWSER'
});
// 4. 处理用户确认(如果需要)
if (beforeunloadHandler.exists()) {
// 延迟导航,等待用户确认
Navigation.delay();
if (!UserConfirmation.confirmed) {
Navigation.cancel();
return;
}
}
// 5. 执行页面卸载和导航
Page.unload();
Navigation.proceed(newUrl);
}
// beforeunload 事件处理器
function beforeunloadHandler(event) {
// 注意:不要添加空事件处理器,会造成不必要的导航延时
// 1. 执行页面切换前的清理工作
DataManager.saveUnsavedData();
EventListeners.cleanup();
Timers.clearAll();
// 2. 如果需要用户确认,设置返回值
if (hasUnsavedChanges()) {
const message = "您有未保存的更改,确定要离开吗?";
event.preventDefault();
event.returnValue = message;
return message;
}
// 3. 如果不需要确认,不设置返回值(避免延时)
return undefined;
}
导航栏预加载

用户明确请求某个网页前,浏览器根据用户的行为预先加载一些可能会访问的资源或页面,以减少页面加载时间,提升网站响应速度 (访问频率、访问时间、书签和收藏夹、地址栏输入补全等措施识别)
渲染流程
渲染进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之互动的网页
HTML => DOM

- 主线程对 HTML 进行解析,形成特定规则的 DOM 结构(HTML 解析不会出错的原因,是因为对于标签不闭合、写错的,浏览器进行了错误处理和异常情况判断)
- 在解析构建 DOM 树时,预加载器会先扫一遍,对于 图片、CSS、JS 等外部资源提前进行网络请求
- script 会改变 DOM 结构,所以会阻碍解析,可以利用 async defer 进行先加载,后延迟执行
CSS => CSS Rule

主线程解析 CSS 并确定 DOM 的节点计算样式,即使未提供任何 CSS,每个 DOM 节点也需要计算本身的默认样式表
布局(Layout Tree)


第一步需要确定元素的位置和大小,主线程会便利 DOM 和 计算样式,创建包含 x、y 坐标和边界框大小等信息的布局树(需要考虑所有会影响元素大小和位置的因素)
光栅化(Rasterization)

- 计算机图形学的关键步骤,将矢量图形转换为像素点,以便在屏幕上显示
- Chrome 早期是直接对视口区域的部分进行光栅化处理,用户滚动网页,那么移动已经光栅化的帧,并通过更多光栅化填充当前视口缺失的部分,但是 需要占据主线程、卡顿和延迟、复杂页面困难
合成(Compositor)

合成线程将页面各个部分分成多层并单独光栅化,并将最终渲染结果发送给 GPU 进行显示,能够 分层渲染、独立于主线程、硬件加速、优化滚动和动画,以此提高页面的渲染性能和响应速度

主线程对布局树进行遍历,根据 layer 顺序生成层树,这里是相同层的元素放到一起,不为每个元素添加层顺序标识,主要是为了避免跨域多层会导致操作速度变慢 (图层树生成算法是什么?)

- 确定图层树顺序后,主线程将信息提交到合成器线程,合成器线程对每个图层进行光栅化处理。对于比较大的图层,会进行分块并进行光栅化,处理后存储到 GPU 显存中
- 合成器将处理好的帧提交给浏览器进程,并发送到 GPU 进行显示图像显示处理
requestAnimationFrame 执行
requestAnimationFrame 是浏览器提供的一个用于动画渲染的 API。在浏览器下一次重绘之前执行指定的回调函数,通常用于实现高性能的动画效果
- 注册回调:调用 requestAnimationFrame(fn),浏览器将 fn 放入一个队列
- 渲染循环:浏览器每次准备重绘(repaint)前,统一执行队列里的所有回调
- 回调时机:回调在布局和绘制之前执行,保证动画和页面渲染同步
- 优化机制:浏览器可根据系统性能自动调整帧率,页面隐藏时不执行回调
特性 | requestAnimationFrame |
普通定时器( setTimeout / setInterval ) |
---|---|---|
同步性 | 与浏览器刷新率同步(通常60fps),动画更流畅 | 不一定与浏览器刷新率同步,可能导致卡顿或丢帧 |
资源利用 | 页面不可见时自动暂停,节省资源 | 页面不可见时仍继续执行,浪费资源 |
执行时机 | 在浏览器重绘前执行,保证动画与页面渲染同步 | 在主线程上执行,容易与其他任务抢占资源,导致动画不流畅 |
自动优化 | 浏览器可根据性能自动调整帧率 | 无法自动调整帧率,固定时间间隔执行 |
适用场景 | 高性能动画、游戏、复杂交互 | 简单任务、定时提醒、轮询等 |
合成器详解
输入事件

屏幕上发生(输入、点击、触摸、滚动)等输入事件,浏览器进程最先接受,但是元素是在渲染进程中。所以浏览器进程会把 事件类型和坐标位置 发送给渲染进程,渲染进程通过查找事件目标或添加监听器来处理事件
非快速滚动区域

定义:合成器线程在处理网页内容时,元素存在事件处理脚本,等待并将 JS 逻辑发送主线程处理
但是对于事件委托,从浏览器角度,整个块级元素都会标记为非快速滚动区域。当输入事件触发,合成器线程必须和主线程通信并等待,流畅度滚动有可能失效,不要大段的 DOM 都用 事件委托。
javascript
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});
passive 可以让合成器线程继续合成页面而无需等待
事件调度合并

一般来说,屏幕刷新率 60HZ,典型的触摸屏设备每秒可传送 60-120 次轻触事件,典型的鼠标每秒可传送 100 次事件,输入事件的保真度高于屏幕刷新的速度
- 如果每秒向主线程发送 120 次
touchmove
等连续事件,那么与屏幕刷新速度相比,系统可能会触发过多的点击测试和 JavaScript 执行。 - 为了尽量减少对主线程的过多调用,Chrome 会合并连续事件(例如
wheel
、mousewheel
、mousemove
、pointermove
、touchmove
),并将调度延迟到下一个requestAnimationFrame
之前 - 系统会立即调度任何离散事件(即单次触发得到结果),例如
keydown
、keyup
、mouseup
、mousedown
、touchstart
和touchend
javascript
window.addEventListener('pointermove', event => {
const events = event.getCoalescedEvents();
for (let event of events) {
const x = event.pageX;
const y = event.pageY;
// draw a line using x and y coordinates.
}
});
开发想要获得合并中每个事件的位置项,以此获得更加准确的位置,可使用 getCoalescedEvents 获取
后续学习
- web.dev 网站性能解析
- 操作系统进程、线程深入
- 网站解析器模型执行流程
- 事件循环机制更深入研究
- 浏览器渲染生命周期具体解析
- Service Worker (独立线程,统筹缓存、控制网络请求)