1、进程与线程
可以这样理解:
- 进程是一个工厂,每个工厂有其独立的资源。
- 线程是工厂中的工人,可能只有一个,可能有好多个。多个工人协同完成工作。工人共享工作资源。
回到硬件上来理解:
- 工厂的资源 -> 系统分配的内存。
- 工厂之间相互独立 -> 进程之间相互独立,也即进程分配到的内存相互独立,无法读到对方内存中的数据。
- 一个工厂有一个或多个工人 -> 一个线程中有一个或多个线程。
- 多个工人协同完成工作 -> 进程中多个线程协同完成工作。即线程之间能互相发送请求与接收结果。
- 工人共享工作资源 -> 进程中所有线程都能访问到相同一块内存,即信息是互通的。
不过在这里要强调一点:一个软件不等于一个进程,一个软件可能包含有多个互相独立的进程。
最后,再用官方的术语描述下进程与线程的差别
- 进程是系统资源分配的最小单位(即系统以进程为最小单位分配内存空间,同时进程是能独立运行的最小单位)
- 线程是系统调度的最小单位(即系统以线程为单位分配cpu中的核。)
tips:
- 进程之间也能互相通信,不过代价比较大。
2、浏览器进程
首先,明确的是:浏览器是多线程的。
以Chrome浏览器为例:(shift+esc)
大家有兴趣的话,也可以打开Chrome的任务管理器测试。由图可知,Chrome中有多个进程(每个tab页面对应一个进程,以及Browser进程,GPU进程和插件进程)。
2.1、 浏览器都有哪些进程
浏览器中的进程分别是:
- Browser进程 : 是浏览器的主进程,负责主控,协调,只有一个,可以看做是浏览器的大脑。 - 负责下载页面的网络文件 - 负责将renderer进程得到的存在内存中的位图渲染(显示)到页面上 - 负责创建和销毁tab进程(renderer进程) - 负责与用户的交互
- GPU进程 : 只有一个。 - 负责3D绘制,只有当该页面使用了硬件加速才会使用它,来渲染(显示)页面。否则的话,不使用这个进程,而是用Browser进程来渲染(显示)页面
- renderer进程:又名浏览器内核,每个tab页面对应一个独立的renderer进程,内部有多个线程。 - 负责脚本执行,位图绘制,事件触发,任务队列轮询等 - 第三方插件进程:每种类型的插件对应一个进程。
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
浏览器是多进程的好处非常明显,如果浏览器是单线程的话,则一个页面,一个插件的崩溃会导致整个浏览器崩溃,用户体验感会非常差。
当然,浏览器有时会将多个进程合并(譬如打开多个空白标签页后,会发现多个空白标签页被合并成了一个进程),如图:
2.1.1、浏览器内核(renderer进程)
renderer进程是多线程的,以下是各个线程的名称及作用(仅列举常驻线程):
那么接下来看看它都包含了哪些线程(列举一些主要常驻线程):
1、GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
2、JS引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3、事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行
4、定时触发器线程
- 传说中的
setInterval
与setTimeout
所在线程- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
5、异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
2.1.2、Browser进程和浏览器内核(Renderer进程)的通信过程
如果自己打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开Tab页的渲染进程),
然后在这前提下,看下整个的过程:(简化了很多)
- Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染
- 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
- 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
- 最后Render进程将结果传递给Browser进程
- Browser进程接收到结果并将结果绘制出来
这里绘一张简单的图:(很简化)
2.2、梳理浏览器内核中线程之间的关系
2.2.1、GUI渲染线程与JS引擎线程互斥
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,
GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
2.2.2、JS阻塞页面加载
从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面。
譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存到队列中,等待JS引擎空闲后执行。
然后,由于巨量计算,所以JS引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。所以,要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
2.2.3、WebWorker,JS的多线程
前文中有提到JS引擎是单线程的,而且JS执行时间过长会阻塞页面,那么JS就真的对cpu密集型计算无能为力么?
所以,后来HTML5中支持了Web Worker
。
MDN的官方解释是:
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面
一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件
这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window
因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误
这样理解下:
- 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
- JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程,只待计算出结果后,将结果通信给主线程即可
而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
2.2.4、WebWorker与SharedWorker
既然都到了这里,就再提一下SharedWorker
(避免后续将这两个概念搞混)
WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享
- 所以Chrome在Render进程中(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的JavaScript程序。
SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用
- 所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。
看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程
3、简单梳理下浏览器渲染流程
- 浏览器输入url,浏览器主进程接管,开一个下载线程,
然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,
随后将内容通过RendererHost接口转交给Renderer进程- 浏览器渲染流程开始
浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:
- 解析html建立dom树
- 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息
- 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。
- 当你在浏览器地址栏里输入内容时,浏览器进程的UI线程会捕捉你的输入内容,如果访问的是网址则UI线程会启动一个网络线程来请求DNS进行域名解析,然后开始连接服务器获取数据;如果你的输入不是网址而是一串关键词,浏览器就知道你是要搜索,于是就会使用默认的配置的搜索引擎来查询
- 再网络线程获取数据后,会通过SafeBrowsing来检查是否是恶意站点,如果是,则会提示一个警告页面,告诉你这个站点有安全问题。(浏览器会组织你的访问,当然你也可以强行进行访问)SafeBrowsing是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全,比如通过查看该站点的ip是否再谷歌的黑名单之内,当返回数据准备完毕并且安全校验通过时,网络线程会通知UI线程我已经准备好了,该你了
- 这个时候UI线程会创建一个渲染器进程(Renderer Thread这是渲染进程的主线程),来渲染页面。浏览器进程通过IPC管道将数据传递给渲染器进程,这时正式进入渲染流程。
- 渲染器进程接收到的数据,也就是html。渲染器进程的核心任务就是把html、css、js、image等资源渲染成用户可以交互的web页面。渲染器进程的主线程将html进行解析,构造DOM数据结构。DOM也就是文档对象模型,是浏览器在其内部的表示形式,是Web开发程序员可以通过JS与之交互的数据结构和API。
- html首先通过tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记进行DOM数构造,在DOM数构造过程中会创建多个document对象,然后以document的为根节点的DOM数不断进行修改,向其中添加各种元素。html代码中通常会引入额外的资源,比如图片、css、js脚本等。图片和css这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为它们不会影响DOM的生成。但当html解析过程中遇到script标签,就会停止html解析流程,转而去加载解析并执行JS。因为浏览器并不知道JS执行是否会改变当前页面的HTML结构,如果JS代码里用了document.write方法来修改html,那之前的html解析就没有任何意义了,这也就是为什么我们一直说要把script标签放在合适的位置,或者使用async或defer‘属性来异步加载执行JS
- 在html解析完成后我们就会获得一个DOM Tree,但我们还不知道DOM Tree上的每个节点应该长什么样子,主线程需要解析CSS并确定每个DOM节点的计算样式,即使你没有提供自定义的CSS样式,浏览器也会有自己默认的样式表
- 在知道DOM结构和每个节点的样式后,我们接下来需要知道每个节点需要放在页面上的那个位置,也就是节点的坐标和该节点需要占用多大区域,这个阶段被称为layout布局。主线程通过遍历DOM和计算好的样式来生成Layout Tree,Layout Tree上的每个节点都记录了x,y坐标和边框尺寸。这里需要注意的是Dom Tree和Layout Tree并不是一一对应的。比如设置了dispkay:none的不会出现在Layout Tree上,而在before伪类中添加了content值的元素,content里的内容会出现在Layout Tree上而不会出现在DOM Tree里。这是因为Dom是通过html解析获得并不关心样式,而layout Tree是根据DOM Tree和计算好的样式来生成。Layout Tree是和最后展示在屏幕上的节点是对应的。
- 现在我们已经知道元素的大小形状和位置,这还不够,我们还需要知道以什么样的顺序绘制(paint)这个节点。举例来说:z-index属性会影响节点绘制的层级关系,如果我们按照DOM的层级结构来绘制页面,则会导致错误的渲染。为了保证在屏幕上展示正确的层级,主线程遍历Layout Tree创建一个绘制记录表(Paint Record),该表记录了绘制的顺序,这个阶段被称为绘制(paint)。
- 现在知道了文档的绘制顺序,终于到了该把这些信息转换成像素点显示在屏幕上的时候了。这种行为被称为栅格化(Rastering)。当Layer Tree生成完毕和绘制顺序确定后,主线程把这些信息传递给合成器线程,合成器线程将每个图层栅格化。由于每个图层可能像页面的整个长度一样大,因此合成器线程将他们切割成许多图块(tiles),然后将每个图块发送给栅格化线程(Raster Thread)。栅格化线程栅格化每个图块并将他们存储在GPU内存中。(现在的Chrome使用了一种复杂的栅格化流程,叫做合成(Composting)。合成是一种将页面的各个部分分成多个图层,分别对其进行栅格化(Rastering),并在合成器线程(Compositor Tread)中单独进行合成页面的技术。简单来说就是页面的所有元素按照某种规则进行分图层,并把图层都栅格化好了,然后只需要把可视区域的内容组合成一帧展示给用户即可。)
- 当图块栅格化完成后,合成器线程将收集称为“draw quads”的图块信息,这些信息里记录了图块在内存中的位置和在页面的那个绘制绘制图块的信息。根据这些信息合成器线程生成了一个合成器帧(Compositor Frame),然后合成器帧通过IPC传送给浏览器进程,接着浏览器进程将合成器帧传送到GPU,然后GPU渲染展示到屏幕上。这个时候才能看到页面的内容。
- 当你的页面发生变化,比如你滚动了当前页面,则会生成一个新的合成器帧,新的合成器帧再传给GPU,然后再次渲染到屏幕上。
- 当我们改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style)布局(Layout)绘制(Paint)以及后面所有流程,这种行为我们称为重排。
- 当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这个就是重绘。
我们可以发现重拍和重绘都会占用主线程,还有另外一个东西也是运行再主线程上的,就是JS。既然他们都是在主线程上运行就会出现抢占执行时间的问题,如果你写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式计算布局和绘制的操作。我们知道用户在每秒60帧的数据进行刷新时才不会觉得卡顿,如果你在运行动画时还有大量的JS任务需要执行,当在一帧的时间内布局绘制还有剩余时间JS就会拿到主线程的使用权,如果JS执行时间过长,就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面的卡顿。
解决方法:
- 通过requestAnimationFrame()来帮助我们解决这个问题。requestAnimationFrame这个方法会在每一帧被调用,通过API的回调我们可以把JS运行任务分成一些更小的任务快(分到每一帧),在每一帧时间用完前暂停JS执行,归还主线程,这样的话再下一帧开始时,主线程就可以按时执行布局和绘制。React最新的渲染引擎React Fiber就是用到了这个API来做了很多优化
- 第二个优化方法:通过上面讲的流程我们知道栅格化的真个流程是不占用主线程的,只再合成器线程和栅格线程中运行,这就意味着他不需要和JS抢占主线程。我们之前提到过如果反复进行重绘和重排可能会导致掉帧,这是因为可能JS执行阻塞了主线程,而CSS中有个属性叫Transform,通过该属性实现的动画不会经过布局和绘制,而是直接运行再合成器线程和栅格线程,所以不会收到主线程中给JS执行的影响。更重要的是通过Transform实现的动画由于不需要经过布局绘制样式计算等操作,所以节省了很多运算时间(方便实现负责的动画),我们平常使用的动画效果:Position、Scale、Rotation这些都是可以通过Transform代替的
这个视频也讲的很清楚:https://www.bilibili.com/video/BV1x54y1B7RE
4、什么时候会发生回流和重绘
- 当页面的某部分元素发生了尺寸、位置、隐藏发生了改变,页面进行回流。得对整个页面重新进行布局计算,将所有尺寸,位置受到影响的元素回流。
- 当页面的某部分元素的外观发生了改变,但尺寸、位置、隐藏没有改变,页面进行重绘。(同样,只重绘部分元素,而不是整个页面重绘)
回流的同时往往会伴随着重绘,重绘不一定导致回流。所以回流导致的代价是大于重绘的。
回流
重绘
3.1、那么具体什么操作会引起回流呢:
- 页面初始化渲染
- 窗口的尺寸变化
- 元素的尺寸、位置、隐藏变化
- DOM结构发生变化,如删除节点 - 获取某些属性,引发回流
- 很多浏览器会对回流进行优化,一定时间段后或数量达到阕值时,做一次批处理回流。
- 当获取一些属性时,浏览器为了返回正确的值也会触发回流,导致浏览器优化无效,有: 1. offset(top/bottom/left/right) 2. client (top/bottom/left/right) 3. scroll (top/bottom/left/right) 4. getComputedStyle() 5. width,height
- 其次,字体大小修改及内容更新也会导致回流
频繁的回流与重绘会导致频繁的页面渲染,导致cpu或gpu过量使用,使得页面卡顿。
3.2、那么如何减少回流呢
1. 减少逐项更改样式,最好一次性更改style,或是将更改的样式定义在class中并一次性更新
2. 避免循环操作DOM,而是新建一个节点,在他上面应用所有DOM操作,然后再将他接入到DOM中
3. 当要频繁得到如offset属性时,只读取一次然后赋值给变量,而不是每次都获取一次
4. 将复杂的元素绝对定位或固定定位,使他脱离文档流,否则回流代价很高
5. 使用硬件加速创建一个新的复合图层,当其需要回流时不会影响原始复合图层回流
3.3、如何才能使用硬件加速
1. translate3d,translateZ
2. opacity属性
硬件加速时请使用z-index
具体原理是这样的:
当一个元素使用了硬件加速,在其后的元素,若z-index比他大或者相同,且absolute或fixed的属性相同,则默认为这些元素也创建各自的复合图层。
所以我们人为地为这个元素添加z-index值,从而避免这种情况
这个视频也讲的很清楚:https://www.bilibili.com/video/BV1x54y1B7RE
参考链接: