关于前端:再理解reflow重排和repaint重绘

52次阅读

共计 3112 个字符,预计需要花费 8 分钟才能阅读完成。

前言

咱们都晓得浏览器中有“重绘”和“重排”两个概念,但浏览器对于咱们是一个黑盒,咱们很难真正弄清楚其实在的代码逻辑如何,除非去钻研浏览器内核源码。

这里本文我也是联合了一些实践经验后提出本人对 重绘、重排 的运作的新的猜想,如有谬误欢送斧正。

怎么了解重排重绘

在本节,咱们抛出 3 个独立的概念:

  • js 执行
  • dom 重排和重绘
  • UI 渲染

先说论断:上述 3 个概念,对于咱们了解重排重绘到底是什么有着至关重要的作用。如上 3 个概念其实是 3 个独立的事件。

  • js 执行是指的运行 js 代码,当然你还其实能够在其中调用 domapi(这种调用尽管会穿透到 c ++ 层,但仍然跟 js 执行占用一个线程);
  • dom 重排和重绘是指的浏览器 DOM 模型对象外部的一种数据结构变动,能够了解为对 dom render tree 这个数据结构的某个子树的从新生成,但它并不意味着你在视觉上能够看到重排重绘后的样子
  • UI 渲染才是真正的进行了视觉上的绘制。只有 UI 渲染后能力肉眼看到 dom 重排重绘后的后果。

上面,咱们来别离解释这些概念。

js 执行

js 执行咱们在网络上很多文章都有学习过了。js 的执行是通过 js 执行引擎线程来执行的。而且这个线程与 UI 渲染线程互斥,因而,当咱们 js 代码运行期间,是不可能进行 UI 渲染的。

浏览器给咱们 js 提供了很多 dom api,让咱们能够去操作浏览器中的 dom 元素,例如扭转 dom 元素的宽度属性,扭转其色彩款式等等。

dom 对象是浏览器底层 C++ 实现的一个对界面上 UI 元素的形象。那么,当咱们 JavaScript 对所谓的 dom api 进行操作的时候,则实际上每次 api 调用实际上是在批改底层 C++ 维持的 dom 对象的属性。

例如:

ele.style.width = '100px'

或者

ele.style.backgroundColor = 'red'

像下面这种操作,要比咱们设置一般的属性如 window.a = 1 要耗时很多。因为批改 dom 属性会穿透到 c ++ 层。


起源:https://www.zhihu.com/questio…

重排和重绘

咱们从网络上文章都曾经晓得重排 (回流) 和重绘的概念。例如,如下行为会引发回流:

1、增加或者删除可见的 DOM 元素;
2、元素地位扭转;
3、元素尺寸扭转——边距、填充、边框、宽度和高度
4、内容扭转——比方文本扭转或者图片大小扭转而引起的计算值宽度和高度扭转;
5、页面渲染初始化;
6、浏览器窗口尺寸扭转——resize 事件产生时;

如下行为会引发重绘:
如:只是影响元素的外观,格调,而不会影响布局的款式,比方 background-color。

起源:https://www.jianshu.com/p/b27…

但回流和重绘到底是何时产生的,大家是否有思考过呢?比方这样一段代码:

ele.style.flex = 1 // 假如 ele 的父元素是 display:flex

那么,你将 ele 元素的 flex 款式设置为 1,即自适应宽度。那么你在何时能够拿到 ele 元素的实在渲染宽高呢?如果你同步来拿,如这样写法:

ele.style.flex = 1
console.log(ele.offsetWidth)

这样是否能够立即拿到 ele 他实在渲染宽高呢?你在 console.log 这句代码执行的这一刻,浏览器中是否曾经依照 flex:1 的预期画好了 UI 界面呢?对于重排重绘到底在线程的什么阶段产生,UI 渲染何时产生,咱们在后文重点解说。

UI 渲染

所谓 UI 渲染,就是通过 UI 线程来将此时 dom 的最新状态,渲染成浏览器中的可视的样子。

UI 渲染线程跟 JS 执行线程永远是互斥的,即当你 js 执行时,UI 必然不能渲染;当你 UI 渲染时,js 必然也无奈

到底何时触发重排重绘

那么,当咱们批改完一个元素的“宽度”或“色彩”,浏览器会立即将批改渲染到页面上吗? 实际上不是的,为了解释 js 代码执行、重排重绘、以及浏览器到底啥时候往 UI 下来绘制咱们的界面这些步骤,那么咱们要搬出一张图了:

首先,浏览器的 js 引擎负责执行咱们的 js 代码(依照宏工作和微工作的执行规定来执行);而当 js 引擎线程闲暇时,浏览器渲染引擎线程才得以有机会失去执行(即上图中红色方块那个地位,示意 js 线程此时闲暇了)。不过,红色方块右侧的彩色开关也不是始终关上的,他遵循 60 帧每秒的一个帧率来关上和敞开,即每 (1000/60) 毫秒关上一次。因而,咱们总结一下浏览器这几个概念的执行步骤:

1、首先,浏览器事件循环会一直的执行 js 代码和 js 宏工作回调,当然如果每次宏工作执行结束后若微工作队列有工作,则清理微工作队列。
2、事件循环如此往返,必然有短暂闲暇时刻(即图中红色方块地位)。每当 js 主线程闲暇时刻,则有机会去依照帧率决定是否切到“UI 渲染线程”去绘制界面(规定就是 60 帧每秒的频率关上该开关)
3、如果,你的 js 代码在主线程中执行过程中,有对元素尺寸等的 api 操作(例如批改 width),那么,js 主线程中这个 dom 批改会将 c ++ 层中的 dom 对象上 width 属性改掉,且会 ” 触发 ”c++ 对这片渲染树的 reflow。
为什么是带引号的“触发”呢,那是因为浏览器会做优化:尽管你扭转了 dom 元素的 width,然而你此时 js 还未执行完,那么 UI 渲染线程必然无奈执行。既然 UI 渲染都无奈执行,所以界面也无奈绘制,因而浏览器认为,他也没必要对你的 width 进行 reflow 重排。因而,浏览器仅仅把你的 width 设置给缓存到队列 —– 等到真正要渲染 UI 的时候再 reflow 就好。

4、浏览器做的挺好了,他的思路没故障。但有一种状况叫做“不可中断的回流”Uninterruptible reflow。这种状况回流会同步产生(即浏览器没方法缓存,必须要做完 reflow 的动作后再往下执行)。起源:https://developer.mozilla.org…

例如你读取了元素的宽:

ele.style.flex = 1
console.log(ele.offsetWidth)

那么,原本下面那一句 flex:1 的动作浏览器是能够缓存的,这样主线程能够立即执行下方代码。但因为你间接调用了 offsetwidth,那么这一句会立即触发浏览器 dom 清空回流操作,即把之前的 flex:1 施行。因而你的代码会阻塞在 ele.offsetWidth 这里期待回流实现,从而拿到正确的 offsetwidth 值(至于回流是产生在主线程还是 ui 线程都并不重要了,总之会阻塞你主线程代码)。这也就是回流性能低的起因了。

5、不论你 js 执行过程中有没有触发上述同步的回流。当你 js 闲暇,那么此时 ui 线程失去机会渲染时,都会看看回流重绘队列里有没有须要重排重绘的操作,有则执行他们并进行 UI 绘制。

到底什么样的代码性能会差?

在网络上,咱们常常会听到说,不要频繁操作 dom 免得升高性能。其实这里有个歧义:即到底是因为什么影响到性能的呢?

便是因为每个 dom 操作 api 实际上要波及到去批改底层 c ++ 对象,这外面便会有较大的性能损耗。不过呢,其实

只操作 dom,性能损耗还好

比方 appendDom 到页面里。其实跟你放到 fragment 再放成果差不多。
因为浏览器做了优化,你既然不读,那我都不要立即就重排,我等你操作完了再重排。

这里仅仅损耗的是 c ++ 通信损耗

不仅操作 dom,还读取 dom 的特定属性

这个影响比拟大。因为会触发同步的重排重绘。

通过 fragment 来防止。

有一些,你不要循环里读,你读一次就够了。那就把它存到变量里。

尽管没有冗余的重排(被浏览器优化了),然而往页面里塞了 100000 个 dom,

此时,性能问题其实呈现在浏览器的 UI 渲染上,因为页面中 rendertree 太多,重排工夫也多,且往 ui 上绘制也很耗时。

正文完
 0