前言
我们都知道浏览器中有 “重绘” 和 “重排” 两个概念,但浏览器对于我们是一个黑盒,我们很难真正弄清楚其真实的代码逻辑如何,除非去研究浏览器内核源码。
这里本文我也是结合了一些实践经验后提出自己对 重绘、重排 的运作的新的猜测,如有错误欢迎指正。
怎样理解重排重绘
在本节,我们抛出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上绘制也很耗时。