浏览器多进程架构及工作原理
# 前言
常常听到这样的话,前端工程师必须理解浏览器的工作原理
从浏览器输入地址栏短短几秒内做了什么东西
各种代码优化方案为什么能起到优化
以及浏览器的渲染流程
看过辣么多的浏览器工作原理,这家是最好的 参考链接
# 一、浏览器的多进程架构
一个好的程序常常会被划分成几个相互独立又彼此配合的模块,以chrome浏览器为例
它是由多个进程组成,每个进程都有自己的核心职责,它们相互配合完成浏览器的整体功能
每个进程又包含多个线程,一个进程内的多个线程也会协同工作,配合所在进程的职责
# 1、进程与线程的解释
# 2、浏览器的架构
如果需要开发一个浏览器,它可以是单进程多线程应用,也可以是使用IPC通信的多线程应用
以Chrome浏览器为例进行说明
- Browser Process
- 负责地址栏,书签栏,进退按钮等部分的工作
- 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问
- Renderer Process
- 负责一个 tab 内关于网页呈现的所有事情 解析HTML及css,将解析结果显示出来
- JS解释器:用来解析执行JS代码
- Plugin Process
- 负责一个网页内用到的插件,如flush
- GPU Process
- 负责处理GPU相关的任务 如果Chrome浏览器运行的硬件设备不够好时,下面这些进程会合并到Browser Process
- Networking Process :完成网络请求,例如http请求,具有平台无关的接口,可以在不同平台上工作
- UI后端 Process:用来绘制 组合选择框,对话框基本组件(alert,confirm),具有不特定于某个平台的通用接口,底层使用操作系统的用户接口
- Storage Process 属于持久层,浏览器需要在硬盘中保存类似Cookie的各种数据,HTML5定义了 web database技术
# 3、不同进程负责的浏览器区域示意图
Chrome 提供任务管理器
查看当前浏览器中运行的所有进程及每个进程占用的系统资源
通过「页面右上角的三个点点点 --- 更多工具 --- 任务管理器」即可打开相关面板,
# 4、Chrome 多进程架构的优缺点
# 优点
- 一个渲染进程出问题不会影响其他进程
- 更为安全,在系统层面上限定了不同进程的全新
# 缺点
- 不同进程间的内存
不共享
不同进程的内存常常需要包含相同的内容 - 为节省内存,Chrome 限制了最多的进程数,最大进程数量由设备的内存和CPU能力决定 当达到这个限制时,新打开的Tab会共用之前同一个站点的渲染进程
测试了一下在 Chrome 中打开不断打开知乎首页,在 Mac i5 8g 上可以启动四十多个渲染进程,之后新打开 tab 会合并到已有的渲染进程中。
Chrome 把浏览器不同程序的功能看做服务,这些服务可以很方便的分割为不同的进程或者合并为一个进程。 以Broswer Process 为例,如果Chrome运行在强大的硬件上,它会分割不同的服务到不同的进程,这样Chrome整体的运行会更加的稳定,如果运行到硬件条件不是那么好的设备上,这些服务会合并到同一个进程中运行,这样可以节省内存
也就是说,不同性能的硬件,进程的划分也会不同
# 5、iframe的渲染 -- Site Isolation
Site Isolation 机制从 Chrome 67 开始默认启用。这种机制允许在同一个 Tab 下的跨站 iframe 使用单独的进程来渲染,这样会更为安全。
# iframe 会采用不同的渲染进程
回到最开始的问题
# 二、导航过程发生了什么
我们知道浏览器Tab外的工作主要用Browser Process 控制的,Browser Process 又对这些工作进一步划分,使用不同线程进行处理
- UI thread : 控制浏览器上的按钮及输入框
- network thread:处理网络请求,从网上获取数据
- storage thread:控制文件等的访问
处理过程
# 1. 处理输入
- UI thread 判断用户输入的是URL还是query
# 2. 开始导航
- 当用户点击回车键,UI thread 通知network Thread 获取网页内容,并控制tab的spinner 展现,表示正在加载中
- network thread 会执行
DNS
(域名系统)查询,随后为请求建立TLS
(安全传输层协议)链接 - 如果network thread 接收到重定向请求如301,
network thread
会通知UI thread
服务器要求重定向,之后,另外一个url请求会别触发
# 3. 读取响应
- 请求响应返回时,network thread 会依据
Content-Type
及MiMe Type sniffing
(媒体类型)来判断响应内容的格式 - 如果响应内容的格式是HTML,则将这些数据传递给renderer Process ,如果是zip 或其他文件,会把相关数据传递给下载器 -SafeBrowsing 触发安全浏览检查,如果域名请求内容匹配到一个恶意站点 network Thread 会展示一个警告页,此外 CORB 检测也会触发确保敏感数据不会被传递给渲染进程。
# 4. 查找渲染进程
- 当上述所有检查完成,network Thread 确信浏览器可以导航到请求网页,network thread 会通知UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页渲染
由于网络请求获取响应需要时间,这里其实还存在着一个加速方案。当 UI thread 发送 URL 请求给 network thread 时,浏览器其实已经知道了将要导航到那个站点。UI thread 会并行的预先查找和启动一个渲染进程,如果一切正常,当 network thread 接收到数据时,渲染进程已经准备就绪了,但是如果遇到重定向,准备好的渲染进程也许就不可用了,这时候就需要重启一个新的渲染进程。
# 5. 确认导航
- 讲过了上述过程,数据以及渲染进程都可用了,Browser Process 会给renderer process 发送IPC(进程间通信)消息来确定导航,一旦Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载开始
- 此时,地址栏会更新,展示出新页面的网页信息。history tab 会更新,可通过返回键,返回上一个页面,为了让关闭tab或者窗口便于恢复,这些信息会放在硬盘中。
# 6. 额外的步骤
- 一旦导航被确认,renderer process 会使用相关资源渲染页面,渲染结束(包括iframe的都触发了onload)发送IPC信号到Browser process ,UI thread 停止tab中loadding
- 所有的js代码都是由renderer process 控制的,所以再浏览网页内容基本上不设计其他进程。
beforeunload
时会涉及到 Browser process 与Renderer prcess 的交互,当关闭页面时(关闭或者刷新)Browser process需要通知renderer process 进行相关处理 - 如果导航由 renderer process 触发(link="www.baidu.com"或者location.href)renderer process 首先会检查是否有
beforeunload
事件,然后由renderer process传递给Browser process - 如果导航到新的网站,会启用一个新的renderer process 渲染页面,老进程会用来处理类似
unload
等事件
浏览器发送IPC消息到新渲染进程渲染页面,同时通知旧渲染进程卸载
- 使用 Service worker 的流程会有些许不同
当Service Worker 被注册时,其作用域会被保存,当有导航时,network thread会再注册的Service worker 的作用域中检查相关域名,如果存在相应的Service Worker,UI thread 会找到一个renderer process 来处理相关代码 Service Worker 可能会从 cache 中加载数据,从而终止对网络的请求,也可能从网上请求新的数据。
# 7. 关于页面生命周期
# 三、 Renderer process 工作流程
渲染进程的核心目的是将HTML、CSS、JS转化为可交互的web页面,渲染进程主要包含一下线程
- 主线程 Main thread
- 工作线程 Worker thread
- 排版线程 Compositor thread
- 光栅线程 Raster thread
# 渲染流程及各线程职责
- 构建dom
- 渲染进程接受到导航的确认消息,开始接受HTML数据,主线程解析文本字符串为DOM
- 加载次级资源
- css js 额外资源 需要从网络上或者cache中获取,主进程在构建DOM过程中会逐一请求他们,为了加速 preload scanner会同时进行
- html中存在
img
link
等标签,preload scanner 会将这些请求传递给browser process 中的newwrok thread 进行相关资源下载
- js的下载与执行
- 当遇见
<script>
标签时,渲染进程会阻塞 - 如果资源内不会改变
DOM
结构 可使用async
defer
等属性,浏览器就会异步加载js,不会造成阻塞
- 当遇见
- 样式计算
- 渲染进程主线程计算每一个元素节点的最终样式值
- 获取布局
- 主线程遍历 DOM 及 对应元素的样式,构建出布局树
- 渲染一个完整的页面,需要知道每个节点的具体演示及其位置
- 遍历DOM及相关元素的计算样式,主线程会构建出包含每个元素及坐标信息的盒子大小的布局树。
- 绘制各元素
- 主线程依据布局树构建绘制记录
- 合成帧
- 主线程遍历布局树创建层树(layer tree),添加
will-change
css属性会被看做单独的一层 - 确定渲染顺序,主线程会把信息传递给接合器线程,合成器会栅格化每一层。有的层可以达到整个页面的大小 ,因此,合成器将它们分成了多个磁贴,并将每个磁贴发送到栅格线程
- 栅格线程会栅格化每个磁贴并存储在GPU中
- 一旦磁贴被光栅化,合成器线程会手机为绘制四边形磁贴信息以绘制合成帧
- 合成帧随后会通过IPC消息传递给 Browser process
- 滚动页面,合成器线程会创建另外一个合成帧发送给GPU再次进行渲染
- 合成器的优点在于,其工作无关主线程.合成器线程不需要等待样式计算或者 JS 执行
- 主线程遍历布局树创建层树(layer tree),添加
# 4、浏览器对事件的处理
# 事件处理
对tab内内容的处理由Renderer process 控制
- 用户触发注入touch等手势,Browser process会发送时间类型及相应的坐标个Renderer process,Renderer process随后会找到事件对象并执行绑定再上面的事件处理函数
- 涉及 non-fast scrollable region(绑定事件区域) 的事件,合成器线程会通知主线程进行相关处理
- web 开发中常用的事件处理模式是事件委托,基于事件冒泡,我们常常在顶层绑定事件
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault();
}
});
2
3
4
5
整个页面都成了 non-fast scrollable region(绑定事件区域) 了,流畅的合成器独立处理合成帧的模式就失效了
解决方法1:
@param passive:true //能让浏览器即监听相关事件,又让组合器线程在等主线程响应前构建新的组合帧
@event.cancelable //使用 `passive: true` 可以实现平滑滚动,但是垂直方向的滚动可能会先于`event.
//preventDefault()`发生,此时可以通过 `event.cancelable` 来防止这种情况
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
}
}, {passive: true});
2
3
4
5
6
7
8
解决方法2:
/*使用css属性 `touch-action` 来完全消除事件处理器的影响*/
#area {
touch-action: pan-x;
}
2
3
4
# 查找事件对象
主线程依据绘制记录查找事件相关元素 当组合器线程发送输入事件给主线程时,主线程首先会进行
命中性测试
(hit test) 查找对应的事件,命中测试会基于渲染过程生成绘制记录(paint records),查找事件发生坐标下存在的元素
# 事件的优化
一般屏幕的刷新速率是60fps ,但是某些事件的触发量会超过这个值,出于优化的目的,Chrome会合并连续的事件(如Wheel,mousewhell,mousemove,pointmove,touchmove),并延迟到下一帧渲染的时候执行
而如 keydown,keyup,mouseup,mousedown,touchstart和touchend等非连续性事件则会立即触发
合并事件虽然能提示性能,但是如果你的应用是绘画等,则很难绘制一条平滑的曲线了,此时可以使用
getCoalescedEvents
API 来获取组合的事件。示例代码如下:
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.
}
});
2
3
4
5
6
7
8