JS的回流和重绘

JavaScript011

JS的回流和重绘,第1张

一、首先我们简单了解下浏览器的渲染过程:

1.解析HTML抽象DOM Tree

2.抽象出Render Tree

3.布局(layout)render tree

4.绘画render tree

HTML解析成DOM,抽象DOM Tree

解析html文档并将解析的元素转换为dom树中的实际dom节点。

把CSS解析成CSSOM,抽象CSSOM Tree

当浏览器解析dom的时候,遇到link标签,引用外部的css样式表,引擎会将css抽象成cssom

构建渲染树(Render Tree)

DOM和CSSOM合并就产生了Render Tree。构建渲染树大致步骤:

①从dom树的根开始,遍历每个可见节点。

②对于每个可见节点,浏览器找到适当的匹配cssom规则并应用它们。

③它会发布带有内容和计算样式的可见节点。

④每个渲染器代表一个矩形区域,通常对应于一个节点的CSS框。

渲染树的布局

①当渲染器被创建并添加到树中时,它没有位置和大小。计算这些值称为布局。

②布局是一个递归过程,从根元素开始,也就是html,每个渲染器都会去计算他自己的位置和大小,由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。

绘制渲染树

遍历渲染器树,调用渲染器的paint()方法,然后展示。

二、回流和重绘(reflow和repaint)

1. 理解回流和重绘

回流: 当元素的结构、位置、尺寸或某些属性发生改变时,浏览器需要重新计算样式和渲染树;

重绘:当元素发生的改变只影响了节点的一些样式(背景色,边框颜色,文字颜色等),浏览器只需要将新样式赋予给这个元素并重绘它。

需要注意的是, 回流必将引起重绘,重绘不一定会引起回流。

2.如何触发回流

(1)页面首次渲染

(2)DOM树变化(如:增删可见dom节点)

(3)浏览器窗口resize

(4)元素尺寸或位置发生改变

(5)查询布局信息,包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、调用了getComputedStyle()或者IE的currentStyle时,浏览器为了返回最新值,会触发回流。

3. 如何触发重绘

背景色、颜色、字体改变,(例如:color、background-color、visibility等,注意:字体大小发生变化时,会触发回流)

三、可减少回流和重绘,优化渲染性能

1.尽量避免改变和多次读取布局属性。如width, height, left, top。

2.除了transforms 或者 opacity属性都会引起重绘,做动画的时候要注意,尽量使用这两个属性;

3.将复杂的节点元素脱离文档流,降低回流成本。

4.缓存布局信息

5.使元素进行动画效果的时候脱离文档流,可先绝对定位,设置好后再恢复。

部分参考:https://segmentfault.com/a/1190000014474575?from=singlemessage&isappinstalled=0

document.write重排整个页面

innerHTML可以重绘页面的一部分

1、构建DOM树(parse):渲染引擎解析HTML文档,首先将标签转换成DOM树中的DOM node(包括js生成的标签)生成内容树(Content Tree/DOM Tree);

2、构建渲染树(construct):解析对应的CSS样式文件信息(包括js生成的样式和外部css文件),而这些文件信息以及HTML中可见的指令(如<b></b>),构建渲染树(Rendering Tree/Frame Tree);

3、布局渲染树(reflow/layout):从根节点递归调用,计算每一个元素的大小、位置等,给出每个节点所应该在屏幕上出现的精确坐标;

4、绘制渲染树(paint/repaint):遍历渲染树,使用UI后端层来绘制每个节点。

重绘(repaint或redraw):当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。

触发重绘的条件:改变元素外观属性。如:color,background-color等。

重排(重构/回流/reflow):当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。

重排必定会引发重绘,但重绘不一定会引发重排。

触发重排的条件:任何页面布局和几何属性的改变都会触发重排,比如:

1、页面渲染初始化;(无法避免)

2、添加或删除可见的DOM元素;

3、元素位置的改变,或者使用动画;

4、元素尺寸的改变——大小,外边距,边框;

5、浏览器窗口尺寸的变化(resize事件发生时);

6、填充内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变;

7、读取某些元素属性:(offsetLeft/Top/Height/Width, clientTop/Left/Width/Height, scrollTop/Left/Width/Height, width/height, getComputedStyle(), currentStyle(IE) )

重绘发生的情况:

重绘发生在元素的可见的外观被改变,但并没有影响到布局的时候。比如,仅修改DOM元素的字体颜色(只有Repaint,因为不需要调整布局)

重绘重排的代价:耗时,导致浏览器卡慢。

1、浏览器自己的优化:浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

2、我们要注意的优化:我们要减少重绘和重排就是要减少对渲染树的操作,则我们可以合并多次的DOM和样式的修改。并减少对style样式的请求。

(1)直接改变元素的className

(2)display:none;先设置元素为display:none;然后进行页面布局等操作;设置完成后将元素设置为display:block;这样的话就只引发两次重绘和重排;

(3)不要经常访问浏览器的flush队列属性;如果一定要访问,可以利用缓存。将访问的值存储起来,接下来使用就不会再引发回流;

//例如myElement元素沿对角线移动,每次移动一个像素。到500*500像素的位置结束。timeout循环体中可以这么做

//显然这种方法低效,每次移动都要查询偏移量,导致浏览器刷新渲染队列而不利于优化。好的办法是获取一次起始位置的值,然后赋值给一个变量。如下

(4)使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘;

(5)将需要多次重排的元素,position属性设为absolute或fixed,元素脱离了文档流,它的变化不会影响到其他元素;

(6)如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document;

(7)尽量不要使用table布局。

不管页面发生了重绘还是重排,它们都会影响性能(最可怕的是重排 ,应尽量避免)

DOM:描述该页面的结构

render:描述 DOM 节点 (nodes) 在页面上如何呈现

当 DOM 元素的属性发生变化 (如 color) 时, 浏览器会通知 render 重新描绘相应的元素, 此过程称为 repaint。

如果该次变化涉及元素布局 (如 width), 浏览器则抛弃原有属性, 重新计算并把结果传递给 render 以重新描绘页面元素, 此过程称为 reflow。

这两个过程是很耗费浏览器性能的, 从 IE 系列和 Chrome 渲染页面速度上的差距即可看出渲染引擎计算对应值和呈现并不一定高效, 而每次对元素的操作都会发生 repaints 或 reflow, 因此编写 DOM 交互时如果不注意就会导致页面性能低下.

页面渲染的过程如下:

原文:

https://blog.csdn.net/sinat_37328421/article/details/54575638

https://blog.csdn.net/sophia_little/article/details/79613990

https://blog.csdn.net/qq_34255080/article/details/86235234

目前,比较常见的浏览器内核(渲染引擎)有: WebKit 、 Blink 、 Gecko 、 Trident 、 EdgeHTML ,更多 请看 。

以下是两个主流浏览器内核 WebKit、Gecko 合成 DOM 的过程:

两者整体流程基本相似,在术语方面也有所不同,比如 WebKit 中的 Layout 过程,Gecko 称为 Reflow

比如,React Hooks 中处理副作用时,某些场景下应选择 useLayoutEffect ,而不是 useEffect 的原因正是为了减少回流重绘的过程。

「回流」也有称作「重排」的。

在很长一段时间里,显示器的刷新率多数为 60Hz,即使到现在仍然是占多数。

刷新率(RefreshRate),表示单位时间内能够绘制新图像的次数。举个例子,60Hz 的刷新率,表示显示器要在一秒内刷新 60 次图像,换句话说,一次图像的更新要在 16.67ms 内完成。这样才不会造成卡顿。如果超出这个时间,在视角上就会产生卡顿感。

附一个来自网上的图:

还是用 React 举例吧。我们知道 React 16 起采用了全新的 Fiber 架构,就是为了解决大型应用卡顿的问题。

我们知道,浏览器是多进程的,JavaScript 是单线程的。浏览器会为每个标签(Tab)分配一个进程,每个进程由 GUI 渲染线程、JS 线程、定时器线程、网络线程、事件线程多个线程组成。最重要的一点是: GUI 渲染线程与 JS 线程是互斥的 。换句话说,某个时刻它只能执行其中一个线程,等待该线程执行完毕之后,才将执行权交由另一个线程。

那么为什么 React 15 在处理大型应用的时候会卡顿呢?

首先 React 15 架构,由 协调器 (Reconciler)和 渲染器 (Render)组成。而 React 16 在原来的基础上新增了 调度器 (Scheduler),用于调度任务的优先级。

在 React 15 的 Reconciler 中,组件的挂载(Mounting)、更新(Updating)会对应调用 mountComponent 、 updateComponent 方法,它们内部会执行递归操作,以更新子组件。 但是递归执行的缺点是「无法中断」。 假设 JS 线程执行递归耗时超过了 16.67ms ,由于互斥期间 GUI 线程并不能执行任何的操作,等递归完并生成新的虚拟 DOM 之后,触发 DOM 等更新,此时由 GUI 线程进行处理,可能包括回流、重绘的过程,然后才完成一次页面的更新。由于无法满足刷新率的要求,就会产生卡顿感。

在 React 16 新增的 Scheduler 可以使得浏览器有剩余时间的时候通知 React,而且提供了多种调度优先级,使得更高优先级的任务优先进入 Reconciler 阶段。 而 Reconciler 则是利用了 Fiber 这种架构实现了 「可中断的异步更新」 (请注意,这里的异步并不是由 setTimeout 实现的,由于精度问题, setTimeout 实际上最低延迟时间是 4ms ,在这寸土寸金的一帧时间才 16.67ms ,显然是不合理的)。

因此,React 就是采用了这种思路来解决大型应用卡顿问题的。

题外话扯完了,回到本文的主题...

再附上一张源自网上的图:

这几个关键点:

哪些 CSS 属性影响 Layout,哪些影响 Paint 呢?

可以看这个网站 CSS Triggers 。

其中 Change from default 表示从未设置(即 CSS 默认值)到设置为其他值, Subsequent updates 表示属性修改。