浏览器渲染 && 卡顿排查

浏览器渲染 && 卡顿排查

一、浏览器进程与线程

Chrome浏览器使用多个进程来隔离不同的网页,在Chrome中打开一个网页相当于起了一个进程,每个tab网页都有由其独立的渲染引擎实例。

一个页面进程一般包括以下线程:

  1. GUI 渲染线程

GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。

  1. JavaScript引擎线程

JS为处理页面中用户的交互,以及操作DOM树、CSS样式树。但为了避免因为引入了锁而带来更大的复杂性,JS从最初开始就选择了单线程执行。

当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

但根据timeline发现,JS执行时布局、重排和解析html(不包括dom生成)也可能会同时执行,但绘制、重绘与JS一定是互斥的

这也许是浏览器做的优化策略,在JS引擎执行时,渲染引擎也不会完全不工作,而会做一些计算布局及解析html的事情,总之,浏览器在尽可能快的加载页面

  1. 定时触发器线程

浏览器定时计数器并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

  1. 事件触发线程

当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

  1. 异步http请求线程

在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。

二、渲染流程

渲染流程有四个主要步骤:

构建DOM树和CSSOM树 - 解析HTML文档生成DOM树(深度遍历),解析CSS生成CSSOM树

构建Render树 - 根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),

布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置

绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来

以上步骤是一个渐进的过程,为了提高用户体验,渲染引擎试图尽可能快的把结果显示给最终用户。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。

三、DOM树和Render树的联系

Render树是用于显示,那不可见的元素当然不会在这棵树中出现了,譬如 。除此之外,display等于none的也不会被显示在这棵树里头,但是visibility等于hidden的元素是会显示在这棵树里头的。

DOM对象类型很丰富,什么head、title、div,而Render树相对来说就比较单一了,每一个节点我们叫它渲染器renderer。

renderer与DOM元素并不是一一对应,有些DOM元素没有对应的renderer,而有些DOM元素却对应了好几个renderer,对应多个renderer的情况是普遍存在的,就是为了解决一个renderer描述不清楚如何显示出来的问题,譬如有下拉列表的select元素,我们就需要三个renderer:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。

另外,renderer与DOM元素的位置也可能是不一样的。那些添加了 float或者 position:absolute的元素,因为它们脱离了正常的文档流,构造Render树的时候会针对它们实际的位置进行构造。

四、布局与绘制

上面确定了renderer的样式规则后,然后就是重要的显示元素布局了。当renderer构造出来并添加到Render树上之后,它并没有位置跟大小信息,为它确定这些信息的过程,接下来是布局(layout)。

浏览器进行页面布局基本过程是以浏览器可见区域为画布,左上角为 (0,0)基础坐标,从左到右,从上到下从DOM的根节点开始画,首先确定显示元素的大小跟位置,此过程是通过浏览器计算出来的,用户CSS中定义的量未必就是浏览器实际采用的量。如果显示元素有子元素得先去确定子元素的显示信息。

布局阶段输出的结果称为box盒模型(width,height,margin,padding,border,left,top,…),盒模型精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位。

在绘制(painting)阶段,渲染引擎会遍历Render树,并调用renderer的 paint() 方法,将renderer的内容显示在屏幕上。绘制工作是使用UI后端组件完成的。

五、回流与重绘

回流(reflow):当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染。reflow 会从 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。

重绘(repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。

回流必定触发重绘,而重绘不一定触发回流

每次Reflow,Repaint后浏览器还需要合并渲染层并输出到屏幕上。所有的这些都会是动画卡顿的原因。Reflow 的成本比 Repaint 的成本高得多的多。一个结点的 Reflow 很有可能导致子结点,甚至父点以及同级结点的 Reflow 。在一些高性能的电脑上也许还没什么,但是如果 Reflow 发生在手机上,那么这个过程是延慢加载和耗电的。

reflow与repaint的时机:

display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。

有些情况下,比如修改了元素的样式,浏览器并不会立刻 reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。

有些情况下,比如 resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。

五、Web Worker

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2)DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。

(3)通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

(4)脚本限制
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(5)文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

六、优化思路

1.JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器如果遇到了JavaScript,那么它会暂停构建DOM,将控制权交给JavaScript引擎。(从渲染引擎交给了js引擎)。等JavaScript引擎运行完毕,浏览器再从终端的地方回复DOM的构建。

也就是说,如果需要首屏渲染的越快,就更不应搞在首屏就加载js文件,这也是建议script标签放在body标签底部的原因。当然在当下,并不是说script标签必须放在底部,因为你可以给script加上defer和async属性(网络读取完成后马上执行)。

  1. 长耗时的JS代码放到Web Workers中执行

JS代码运行在浏览器的主线程上,与此同时,浏览器的主线程还负责样式计算、布局、绘制的工作,如果JavaScript代码运行时间过长,就会阻塞其他渲染工作,很可能会导致丢帧。前面提到每帧的渲染应该在16ms内完成(FPS要大于等于60才能保证看起来流畅),但在动画过程中,由于已经被占用了不少时间,所以JavaScript代码运行耗时应该控制在3-4毫秒。如果真的有特别耗时且不操作DOM元素的纯计算工作,可以考虑放到Web Workers中执行。

  1. 拆分操作DOM元素的任务,分别在多个frame完成

由于Web Workers不能操作DOM元素的限制,所以只能做一些纯计算的工作,对于很多需要操作DOM元素的逻辑,可以考虑分步处理,把任务分为若干个小任务,每个任务都放到 requestAnimationFrame中回调执行。

  1. 使用React.PureComponent或React.memo()避免不必要的组件重渲染
    React.PureComponent和React.Component类似,但是React.PureComponent在shouldComponentUpdate()中对props和state做了一个浅比较,如果props和state没有变化则不渲染组件。

React.memo()是16.8版本加入的新功能,为使用函数定义的组件提供了类似PureComponent的功能。

  1. 不要使用inline定义的方法或Object为props传值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值