回流和重绘
阅读时间约10min
写在前面
- 浏览器使用流式布局模型 (Flow Based Layout)
- 解析HTML—>DOM树,解析CSS—>CSSOM树,DOM树和CSSOM树结合—>渲染树(Render Tree)
- Layout(回流):根据生成的Render Tree,进行回流(Layout),得到节点的几何信息(位置,大小)
- Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- Display:将像素发送给GPU,展示在页面上
- 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,可能需要多次计算,通常要花3倍时间,所以要避免使用 table 布局
- 渲染树只包含可见的节点
- 不可见的节点:
- 不会渲染输出的节点,如script、meta、link等
- 通过css进行隐藏的节点,如display:none
- 注意:利用visibility和opacity隐藏的节点还是会显示在渲染树上的,只有display:none的节点才不会显示在渲染树上
- 回流必将引起重绘,重绘不一定会引起回流
- 回流比重绘的代价高
一、回流(Reflow)
概念:
当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
回流这一阶段主要是计算节点的位置和几何信息。
举个栗子:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>
第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%
而在回流这个阶段,需要根据视口具体的宽度,将其转为实际的像素值👇
什么操作发生回流:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如 :hover)
- 查询某些属性或调用某些方法
二、重绘(Repaint)
概念:
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它(将渲染树的每个节点都转换为屏幕上的实际像素),这个过程称为重绘。
三、浏览器的优化机制
大多数浏览器都会通过队列化修改并批量执行来优化。
浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,直到过了一段时间或者操作达到了一个阈值,会清空队列,进行一次批处理,这样可以把多次回流和重绘变成一次。
但是当获取布局信息的操作时,会强制队列刷新。
比如访问以下属性或者使用以下方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect
四、减少回流和重绘
CSS
- 避免使用table布局
- 尽可能在DOM树的最末端改变class
- 避免设置多层内联样式
- 将动画效果应用到position属性为absolute或fixed的元素上
- 避免使用CSS表达式(例如:calc())
JavaScript
-
避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性
-
使用文档片段(document Fragment)在当前DOM之外构建一个子树,再把它拷贝回文档
const ul = document.getElementById('list'); const fragment = document.createDocumentFragment(); appendDataToElement(fragment, data); ul.appendChild(fragment);
-
隐藏元素,设置display: none,操作结束后再显示出来,在display属性为none的元素上进行的DOM操作不会引发回流和重绘
function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement('li'); li.textContent = 'text'; appendToElement.appendChild(li); } } const ul = document.getElementById('list'); ul.style.display = 'none'; appendDataToElement(ul, data); ul.style.display = 'block';
-
避免频繁读取引发回流/重绘的属性,如需要多次使用,设置变量
-
对复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流
五、关于渲染树构建、布局及绘制参考
https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=zh-cn
-----------------------------------------学(tu)到(tou)了-----------------------------------------
我个人开了一个微信公众号,每天会分享一些JavaScript技术相关的基础干货!
欢迎用微信搜索【易碎品啊今天吃什么研究所】或者【扫描下方二维码】关注和我一起JavaScript365!❤️