前言
之前在思否读到了一篇大佬关于浏览器多线程的的文章,受益颇多,然而纸上得来终觉浅,于是打算用自己的理解重写一下,也算是对知识的梳理与巩固。
首先贴上大佬的原贴,感谢大佬的分享
# 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
进程与线程
分清进程与线程的区别是一切的基础,这里举个栗子:
— 进程是一个生产工厂,每个工厂之间是独立互不影响的
— 线程是每个生产工厂中的生产流水线
— 一个工厂中可能存在一条或多条生产流水线
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程则是cpu资源调度的最小单位(线程是建立在进程基础上的运行单位,一个进程中可以包含多个线程)
浏览器的多进程
浏览器基本包含以下进程:
Browser进程:
- 浏览器的主要进程,有且只有一个,主要负责:
- 浏览器的页面显示,用户交互(前进、后退、刷新等)
- 负责其余进程的销毁与创建
- 处理网络下载管理、文件访问等
插件进程
- 每种类型的插件(油猴等)单独对应一个进程,当插件需要使用时才会被创建
GPU进程
- 有且最多一个,负责3D绘制
浏览器内核进程
- 主要负责页面的渲染、脚本运行、事件处理等
- 每个Tab页签默认一个进程
这里以Chrome为例:
可以看到每个页签有着自己的进程ID,创建新的页签会开辟独立的进程(可以通过 窗口 -> 任务管理器 进行确认)
但是不代表每个页签一定对应一个进行,Chrome在某种条件下,会自动合并多个页签,那么这几个页签会合并成一个进程
多进程的优势
- Tab之前互不影响,一个页签崩溃了,其余页签依然可以正常浏览
- 插件的崩溃不会影响到整体浏览器
- 多进程充分利用了多核的高性能优势
多进程也意味着更多资源的占用,从一定程度上来说,就是充分利用硬件性能,以空间换性能的意思
浏览器内核(渲染进程)
这是前端操作最需要关心的部分,页面的渲染、脚本的执行、事件循环这些前端的第一次接触对象都在该进程内进行
浏览器的渲染进程是多线程的
渲染进程内的主要线程有以下5个:
GUI渲染线程
- 负责浏览器的渲染工作,解析HTML、CSS构建DOM树、CSSOM树和Render渲染树,元素布局与绘制等
- 当页面发生回流或重绘时,该线程会执行
- GUI线程与JS引擎线程互斥,当JS执行的时候,GUI线程会被挂起,保存在一个队列中,等JS执行完毕时才会再次执行GUI线程
JS引擎线程
- 负责处理Javascript脚本
- 负责Javascript脚本的解析、代码生成、代码执行
- JS引擎会轮询任务队列中的任务(宏任务与微任务),一旦存在,立刻取出并加以执行
- JS引擎线程与GUI线程互斥,所以当JS引擎执行时间过长的情况下,页面的渲染会非常的缓慢,因为GUI在等待JS引擎执行完毕才能继续渲染工作,也就是通常我们所说的线程阻塞
事件触发线程
- 当事件满足触发条件时,会将事件放入执行队列的队尾,等待JS引擎处理
- 因为Js引擎是单线程的原因,所有事件列队中的事件都要等待JS引擎空闲时以先入先出的顺序依次执行
定时触发器线程
- setInterval与setTimeout所在线程
- 负责浏览器定时计数器的计数
- 计时完成后,通知事件触发线程
异步http线程
- 处理AJAX请求
- 当请求完成时,如有回调函数,会将回调函数放入事件队列,再由JS引擎执行
用实际代码来直观的理解一下:
1 var a = 1;
2 setTimeout();
3 ajax();
4 console.log(a);
5 dom.onClick();
JS引擎开始执行代码
运行第一行代码,LHS查询声明变量a,RHS引用为变量a赋值2
运行第二行代码,发现是setTimeout()定时任务,将代码转交给定时触发器线程执行
运行第三行代码,发现是ajax()http异步请求,将代码转交给http异步线程执行
运行第四行代码,输出a变量,继续执行代码
运行第五行代码,dom.onClick()点击事件,将代码转交给事件触发线程执行,代码执行完毕,JS引擎空闲
所有异步代码的回调都会由各自的线程保存,待事件触发条件满足/计时完成 **并且** JS引擎线程空闲时,会将回调函数交给JS引擎线程执行
- 除JS引擎线程外,其余线程各自执行主线程转交的异步代码,等待JS引擎线程空闲时,交还回调函数
- 保存回调函数,在满足事件触发条件时,通知事件循环拿取要执行的回调函数然后执行
Browser进程(主控制进程)与浏览器内核(Renderer进程)的通信关系
如果你在开着活动监视器的情况下,打开一个浏览器,会发现任务进程中多了两个任务: 一个是主控制进程,另一个是打开Tab的渲染进程
那么他们是如果通信的呢:
- Browser进程收到用户请求,开始获取页面资源信息(比如网络下载js、css等资源),然后通过RendererHost接口发送给Render进程
- Render进程的Render接口接收到任务,经过解释后,交给GUI渲染线程准备开始渲染
- GUI渲染线接收到请求,开始加载渲染页面,期间可能需要Browser进程获取网络资源和需要GPU进程帮助绘制
- 会有JS引擎线程操作DOM导致的回流或者重绘
- 最后Render进程将渲染好的结果发送给Browser进程
- Broswer进程接收到结果并绘制页面
一张简化的通信关系图
浏览器的渲染过程
Browser进程拿到Render内容后,渲染会大致分为以下这几个步骤
- 解析HTML,形成DOM树
- 解析CSS(如果有的话),形成CSSOM树
- 结合DOM树与CSSOM树,形成Render渲染树
- 布局Render树,计算元素在浏览器上的位置、大小等信息
- 绘制Render树
- 浏览器将各层信息发送给GPU进程,GPU将各层结合为composite,绘制页面在屏幕上
CSS加载是否会阻塞DOM树的渲染?
首先要知道,CSS文件是由单独的下载线程进行异步下载的
-
浏览器的渲染过程中已经提到过,DOM树是由HTML解析而来的,所以CSS的加载并不会阻塞DOM树的解析
-
但是CSSOM树是由CSS解析构成,所以CSS的加载会阻塞Render树的构成与渲染,Render树的渲染要等待CSS加载完毕并解析成CSSOM树后才能构成Render树进行渲染
这对于性能也有一定的保护作用,当浏览器在加载CSS的时候,可能会修改DOM节点样式,这时如果CSS加载不阻塞Render树渲染的话,就会导致渲染然后之后,CSS才加载完毕,又会产生DOM节点的更新,Render树又需要重绘或者回流,从而造成无效的性能浪费
所以浏览器从设计上做成将DOM树先解析完,等待CSS加载完成解析为CSSOM树后,一起合并成Render树再进行渲染,这也是浏览器的一种优化机制
普通图层与复合图层
解释复合图层之前,要先理解一下层叠上下文,关于层叠上下文可以看我的另一篇文章
浏览器渲染的图层一般包含两大类: 普通图层和复合图层
根元素文档流就是一个默认复合图层
在GPU进程中,各个复合图层都是单独绘制,然后合并为一个composite。就好比我可以双手同时画画,左手负责画一个图层,右手同时画另一个图层,然后一起合并两个图层完成一幅画,可想而知效率比单手画画要高的多,所以合理的创建复合图层是提升渲染性能中很重要的一点
由于复合图层各自独立于文档流,所以每个复合图层中的元素不管怎么幻化,都不会引起其他复合图层的回流、重绘
那么如何创建新的复合图层呢,可以使用硬件加速的方式,声明一个新的复合图层,单独分配资源,硬件加速的方法有:
- 最常用的方式: translate3d(3d转换)、translateZ
- opacity属性\过渡动画,其中注意只有在动画执行的过程中会创建合成层,动画开始前与结束后,元素会回归普通图层
- will-change属性
- video、iframe、canvas等元素
absolute与硬件加速的区别
虽然定位属性可以脱离文档流,但是不代表他会脱离默认复合图层,absolute中元素改变属性时不会改变普通文档流中的Render树,但是当浏览器渲染到最后一步GPU绘制时,是对absolute所在的整个复合图层绘制的,所以浏览器依然会重绘他
复合图层的作用
说了这么多,我们来总结一下复合图层带来的好处:
- 首当其冲就是带来的性能提升,硬件加速让元素成为新的复合图层,节约重绘、回流面积,提升性能
- 硬件加速后图层会交给GPU直接处理,GPU性能可要强得多
但是这不代表我们可以滥用复合图层,如果创建的复合图层过多,GPU会消耗大量的内存,视运行设备的硬件性能,尤其在移动端,反而可能会比不用硬件加速更卡,让优化适得其反。所以要正确、合理的使用硬件加速,在渲染优化与性能消耗之间取得一个平衡点,才能让页面渲染的既快速、又丝滑
浏览器内核中各线程的关系
在了解了浏览器整体的运作机制后,我们回头来梳理一下浏览器内核中各个线程的关系细节
GUI线程与JS引擎线程的互斥
众所周知JavaScript是可以操作DOM的,如果GUI线程与JS线程可以通行运行的话,就可能会出现: JS在修改元素节点,GUI线程同时又在渲染页面,那么到底以谁的结果为准?
为了避免这个问题,浏览器将这两个线程设置成了互斥的关系,当JS线程运行的时候,GUI线程会被挂起,GUI的更新会被保存进一个队列,等待JS线程空闲的时候再立即执行
JS阻塞页面加载
由于上述的线程互斥关系,我们知道了JS线程在运行的时候,GUI线程是不会更新的,那么就会产生一个新的问题: 当JS引擎在做大量计算时,GUI线程就算有更新,也要一直等待JS线程运行完毕,这个过程可能非常久,这取决于JS引擎到底要计算多久
所以从用户体验触发,要极力避免JS引擎执行时间过长,否则会导致页面有严重的渲染阻塞和加载不连贯的问题