浏览器多进程和事件循环详解

参考链接

由于网上的关于浏览器进程和JS进程、JS线程和事件循环之间的关系模糊不清,这里主要是查阅资料进行详细汇总

关于浏览器多进程和JS单线程的介绍:https://juejin.im/post/5a6547d0f265da3e283a1df7
关于Node事件循环的参考:https://juejin.im/post/5c337ae06fb9a049bc4cd218
关于浏览器时间循环的参考:https://juejin.im/post/5b8f76675188255c7c653811
关于宏任务微任务的详细介绍:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

多进程的浏览器

浏览器包含以下进程

  1. Browser进程,浏览器的主进程(负责协调,控制),只有一个
  2. 第三方插件进程
  3. GPU进程,最多一个,用于3D绘制
  4. 浏览器渲染进程(浏览器内核):默认每个tab页面一个进程,互不影响。主要作用为(页面渲染、脚本执行、事件处理)

浏览器组成概览:

浏览器多进程和事件循环详解_第1张图片
image

区分进程和线程

  • 进程是一个工厂,工厂有它的独立资源
  • 工厂之间相互独立
  • 线程是工厂中的工人,多个工人协作完成任务
  • 工厂内有一个或多个工人
  • 工人之间共享空间

转换成专业术语:

  • 进程是系统分配的内存,是cpu资源分配的最小单位(独立的一块内存)
  • 进程之间相互独立
  • 多个线程在进程中协作完成任务
  • 一个进程由一个或者多个线程组成
  • 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
  • 线程是cpu调度的最小单位

每打开一个tab页,相当于创建了一个独立的浏览器渲染进程
通过右上角->更多工具->任务管理器可以查看到:


浏览器多进程和事件循环详解_第2张图片
image

可以查看到进程的类型:


浏览器多进程和事件循环详解_第3张图片
image

多进程优势

  • 单个page 崩溃不会影响整个浏览器
  • 避免第三方插件崩溃影响整个浏览器
  • 多进程充分利用多核优势
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

浏览器内核(渲染进程)

页面的渲染,JS的执行,事件的循环,都在这个进程内进行。
浏览器的渲染进程是多线程的。

渲染进程的多线程

  1. GUI渲染线程
    • 负责渲染浏览器界面,解析HTML\CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列重等到JS引擎空闲时立即被执行。
  2. JS引擎线程
    • 也称为JS内核,负责处理JS脚本程序。例如V8引擎
    • JS引擎线程负责解析JS脚本,运行代码
    • JS引擎等待任务队列中的任务到来,然后加以处理,一个Tab页(渲染进程)无论什么时候都只有一个JS线程在运行JS程序
    • GUI渲染线程和JS引擎线程互斥,所以如果JS执行时间过长,就会造成页面的渲染不连贯,阻塞页面渲染。
  3. 事件触发线程
    • 用来控制事件循环
    • 当JS引擎执行代码例如setTimeOut,或者其他线程(鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    • 由于JS单线程的关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(JS引擎空闲时才会去执行 )
  4. 定时触发器线程
    • setInterval与setTimeout所在线程
    • 浏览器定时计数器并不是由js引擎计数的(因为js引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确)
  5. 异步http请求线程
    • XMLHTTPRequest在连接后新开一个线程请求
    • 检测到状态变更时,如果有设置回调函数,异步线程产生状态变更事件,将这个回调再放入事件队列中,再由js引擎执行。

由于JS可以操作DOM,如果在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致。
如果js执行时间过长,就会阻塞页面。

浏览器Browser进程和浏览器内核的通信过程

  1. Browser进程收到用户请求,首先需要获取页面内容,通过网络下载资源,随后将该任务通过RenderHost接口传递给渲染进程(内核)
  2. 渲染进程接收到消息,简单解释后,交给渲染线程,然后开始渲染
    • 渲染线程接收到请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 当然可能会有JS线程操作DOM(可能造成回流与重绘)
    • 最后渲染进程将结果传递给Browser进程
  3. Browser进程接受到结果并将结果绘制出来

WebWorker

创建Worker时,js引擎向浏览器申请开一个子线程,子线程是浏览器开的,完全受主线程控制,而且不能操作DOM
JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
所以如果有非常耗时的工作,可以单独开一个worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程,只待计算出结果后,将结果通信给主线程即可。

浏览器渲染流程

输入URL,浏览器主进程接管,开启一个下载线程,进行HTTP请求,将响应内容转发给渲染进程。

浏览器多进程和事件循环详解_第4张图片
输入URL的浏览器行为

整个过程被分为9个小块:

  • 提示卸载旧文档
  • 重定向/卸载旧文档
  • 应用缓存
  • DNS域名解析
  • TCP握手
  • HTTP请求处理
  • HTTP响应处理
  • DOM处理
  • 文档装载完成

渲染(上图的Processing阶段)可以分为以下步骤:

  1. 解析HTML建立DOM
  2. 解析CSS建立CSSOM
  3. 合成render树(Layout/reflow)
  4. 绘制render树(paint)
  5. 浏览器将各层的信息发给GPU,GPU将各层合成(composite),显示在屏幕上

load事件与DOMContentLoaded事件的先后

  • DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片
  • 当onload事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成。

CSS加载不会阻塞DOM树解析,但会阻塞render树渲染,因为render树需要css信息,所以css要在头部加载

回流(reflow)与重绘(repaint)

网页生成的时候,至少会渲染一次,用户访问的过程中,还会不断的重新渲染
以下三种情况,会导致网页重新渲染:

  • 修改DOM
  • 修改样式表
  • 用户事件(比如鼠标悬停、页面滚动、输入框输入文字、改变窗口大小等等)

重新渲染,就需要重新生成布局和重新绘制。前者叫做回流也叫重排(reflow)后者叫做重绘(repaint)。重绘不一定需要重排,比如改变某个元素的颜色,就只会触发重绘,因为布局没有改变。但是重排必然导致重绘,比如改变一个元素的位置。

优化

提高网页性能,就是要降低重排和重绘的频率,尽量少触发重新渲染。

DOM变动和样式变动,都会触发重新渲染,但是浏览器已经很智能了,会尽量把所有的变动集中在一起,排成一个队列,然后一次性执行,避免多次渲染。

例如:

div.style.color = 'blue';
div.style.marginTop = '30px';

元素有两个样式变动,但是浏览器只会触发一次重排和重绘
如果写的不好,就会触发两次重排和重绘

div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';
//上面代码对div元素设置背景色后,第二行要求浏览器给出该元素的位置,所以浏览器不得不立即重排。

如果有以下读操作,都会引发浏览器立即重新渲染。

offsetTop/offsetLeft/offsetWidth/offsetHeight
scrollTop/scrollLeft/scrollWidth/scrollHeight
clientTop/clientLeft/clientWidth/clientHeight
getComputedStyle()

所以,从性能角度考虑,尽量不要把读操作和写操作放在一个语句里面

// bad
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";

// good
var left = div.offsetLeft;  //读
var top  = div.offsetTop;   //读
div.style.left = left + 10 + "px"; //写
div.style.top = top + 10 + "px";  //写

一般规则

  • 样式表越简单,重排和重绘就越快。
  • 重排和重绘的DOM元素层级越高,成本就越高。
  • table元素的重排与重绘成本,要高于div元素
1.DOM的多个读操作,应该放在一起,不要两个读操作之间,加入一个写操作

2.如果某个样式是通过重排得到的,那么最好缓存结果,避免下一次用到的时候,浏览器又要重排

3.不要一条条的改变样式,而要通过改变class或者csstext属性,一次性改变样式
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// good 
el.className += " theclassname";

// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";

4.尽量使用离线DOM,而不是真实的网面DOM,来改变元素样式,比如操作Document Fragment对象,完成后再把这个对象加入DOM。再比如使用cloneNode()方法,在克隆的节点商进行操作,然后再用克隆的节点替换原始节点。

5.先将元素设为dispaly:none(需要一次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,就用两次重新渲染,取代了可能高达100次的重新渲染。

6.position属性为absolute或者fixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响,但会引起图层的重绘。

7.只在必要的时候,才将元素的dispaly属性为可见,因为不可见的元素不影响重排和重绘,另外visibility:hidden的元素只对重绘有影响,不影响重排。

8.使用虚拟DOM的脚本库

9.使用 window.requestAnimationFrame()、window.requestIdleCallback() 这两个方法调节重新渲染

10.使用硬件加速的复合图层

普通图层和复合图层

浏览器渲染的图层一般包括两大类:普通图层以及复合图层
普通文档流内可以理解为一个复合图层。其次absolute布局和fixed,虽然可以脱离普通文档流,但它仍然属于默认复合层

可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘。
GPU中各个复合图层是单独绘制的,所以互不影响

将元素变成新的复合图层的方法(硬件加速):

  • css translate3d、translateZ
  • opacity属性/过渡动画,执行过程中才会创建合成层,动画没有开始或者结束后元素还会回到之前的状态
  • will-change属性,