重绘和回流(重排)
本文是对重绘和回流知识的整理,已经熟悉的可直接跳到标题内容
为了方便个人回顾复习,不用在冗长的代码示例中寻找关键信息,本文所有的代码示例都被折叠起来。如果是第一次阅读,建议在阅读过程中展开折叠部分,有助于加深理解和记忆。
一、网页渲染流程
- 解析HTML代码,生成
DOM
树 - 解析CSS代码,生成
CSSOM
树 - 将DOM树和CSSOM树结合,去除不可见的元素(如
display: none
的元素),构建渲染树Render Tree
- 计算布局(回流 | 重排 ),根据
Render Tree
进行布局计算,得到每一个节点的几何信息 - 绘制页面(重绘),GPU根据布局信息绘制
二、概念
重绘(Repaint) :当元素的外观发生改变 ,但没有改变布局(如改变颜色)时,浏览器将重新绘制该元素,这个过程称为重绘。
回流(Reflow,或重排) :当元素的尺寸或者位置发生改变时(如修改元素的宽高),浏览器需要重新计算元素的几何属性,并且可能导致其他元素的几何属性也发生改变。这个过程称为回流。回流涉及到页面布局和元素位置的更新,通常比重绘更消耗性能。
重绘不一定导致重排,但重排一定会导致重绘。
三、什么情况发生重绘和重排
发生重绘:颜色、背景色、边框样式等
color
、border-style
、border-radius
、text-decoration
、box-shadow
、outline
、background
...
发生重排:各种尺寸、位置等
- 页面初始渲染,这是开销最大的一次重排;
- 添加/删除可见的DOM元素;
- 改变元素位置;
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等;
- 改变元素内容,比如文字数量,图片大小等;
- 改变元素字体大小;
- 改变浏览器窗口尺寸,比如
resize
事件发生时; - 激活CSS伪类(例如:
:hover
); - 设置
style
改变属性的样式; - 查询某些属性或调用某些计算方法:
offsetWidth
等
四、怎么减少重绘和重排
浏览器的优化策略
现在的浏览器是有一个优化策略的,它执行js的时候会维护一个渲染队列 ,改变一个容器的样式,导致需要发生回流的时候,这个操作不会立即执行 ,而是会进入渲染队列,如果还有连续的样式修改,会继续进入队列,直到没有样式修改,浏览器会批量化地执行渲染队列中的回流过程 ,这只发生一次回流,而不是每次修改都触发一次。
例子1:仅持续写,有几次回流?
js
<div id="app">Hello</div>
<script>
let app = document.getElementById('app')
app.style.position = 'relative';
app.style.width = '100px'
app.style.height = '200px'
app.style.left = '10px'
app.style.top = '10px'
</script>
不算浏览器初次加载,是一次回流
例子2:写一行读一行,有几次回流?
js
app.style.width = '100px'
console.log(app.offsetWidth)
app.style.height = '200px'
console.log(app.offsetHeight)
app.style.left = '10px'
console.log(app.offsetLeft)
app.style.top = '10px'
console.log(app.offsetTop)
四次回流 ,同步代码从上往下执行,执行打印语句的时候无法跳过。写时进入渲染队列,当要读取几何信息的时候,是会强制执行渲染队列,也就引起了重排,以确保返回的是最新的布局信息。 持续往复... 因此是四次回流。
例子3:全部写完再读,有几次回流?
js
app.style.width = '100px'
app.style.height = '200px'
app.style.left = '10px'
app.style.top = '10px'
console.log(app.offsetLeft)
console.log(app.offsetHeight)
console.log(app.offsetWidth)
console.log(app.offsetTop)
一次回流 。四次写操作依次进入渲染队列,执行第一个打印语句的时候,强制执行渲染队列,引起一次回流。后续渲染队列就空了,虽然继续读,但空了的队列无法引起回流 ,读取值是不引起回流的,读取值引起的有内容的队列执行才会引起回流。
例子4:同一行有写有读,有几次回流?(字节面试题)
js
let el = document.getElementById('app')
el.style.width = (el.offsetWidth + 1) + 'px'
el.style.width = 1 + 'px'
一次回流 。第一行获取dom结构不造成回流。看第二行,从左往右执行,左边会进入渲染队列,但是右边会获取几何信息,offsetWidth
是强制触发渲染队列的执行,而非强制回流。这里渲染队列里面没有东西去让你执行,所以执行第二行,第三行会连续入渲染队列,最后只发生一次回流。
总结 :只要是读取几何信息都会引起渲染队列的强制执行:off-
、client-
、scroll-
。为了减少回流和重绘,可以合理利用浏览器的优化策略,少去读取几何信息,或者统一放到最后读取。
其他方法
1. 样式集中改变:不要逐个修改样式,通过动态添加class。
代码简示
js
document.getElementById('demo').className = 'demo'; // 统一添加/修改样式
css
.demo {
color: red;
background: #ccc;
padding: 15px 20px;
}
2. 离线操作DOM:不直接操作真实的节点。
- none、block优化 :当对DOM节点有较大改动的时候,我们先将元素脱离文档流
display:none
(需要一次重排),然后对元素进行操作,最后再把操作后的元素放回文档流display:block
(需要一次重排)。
代码简示
js
const renderEle = document.getElementById('demo');
renderEle.style.display = 'none'; // 导致重排
// DOM不存在渲染树上不会引起重排、重绘
renderEle.style.color = 'red';
renderEle.style.background= '#ccc';
renderEle.style.marginLeft = '15px';
renderEle.style.marginTop = '15px';
renderEle.style.border = '2px solid #ccc';
renderEle.style.display = 'block';// 导致重排
- Fragment文档碎片优化 :
Fragment
是一种机制,用于在内存中创建一个轻量级的文档碎片,这个文档碎片可以包含多个节点,它不涉及dom结构的实际插入,因此不会触发回流,最后带有批量节点的文档碎片插入到文档中,这样可以减少回流的次数。
代码简示
js
const ul = document.getElementById("box");
const fragment = document.createDocumentFragment(); // 虚拟的文档片段
for (let i = 1; i<=100; i++) {
let li = document.createElement("li");
let text = document.createTextNode(i)
li.appendChild(text)
fragment.appendChild(li); // 虚拟片段不会回流
}
ul.appendChild(fragment)
- cloneNode方法优化 :使用
cloneNode
方法进行深拷贝,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。
代码简示
js
const ul = document.getElementById("box");
const clone = ul.cloneNode(true) // 克隆一份ul,必定是深拷贝,原ul不受影响
for (let i = 1; i<=100; i++) {
let li = document.createElement("li");
let text = document.createTextNode(i) // 创建文本节点
li.appendChild(text)
clone.appendChild(li);
}
ul.parentNode.replaceChild(clone, ul) // (替代品, 被替代品)
3. 脱离文档流:使用 absolute 或 fixed 脱离文档流。
4. 善用内存:在内存中多次操作DOM,再整个添加到DOM树 构建整个ul,而不是循环添加li
代码简示
循环添加li,每一次都会引起重排
html
<div id="demo">
<ul id="father">
<li>我是0号,我后面还有1号、2号、3号、4号、5号</li>
</ul>
</div>
js
const ulEle = document.getElementById("father");
let arr = [];
setTimeout( () => {
arr = "我是0号,我后面还有1号,2号,3号,4号,5号", "我是2号", "我是3号", "我是4号", "我是5号"]; // 我是动态获取的
arr.forEach(element => {
const childNode = document.createElement('li');
childNode.innerText = element;
ulEle.appendChild(childNode);// 每一次都会引起重排(重排会引起重绘)
})
},1000)
构建整个ul,只引起一次重排
html
<div id="demo"></div>
js
const ulEle = document.getElementById("demo");
const childUlNode = document.createElement('ul');
let arr = [];
setTimeout(() => {
arr = ["我是0号,我后面还有1号,2号,3号,4号,5号","我是1号", "我是2号", "我是3号", "我是4号", "我是5号"]; // 我是动态获取的
arr.forEach(element => {
const childLiNode = document.createElement('li');
childLiNode.innerText = element;
childUlNode.appendChild(childLiNode);
})
},1000)
ulEle.appendChild(childUlNode);// 只会引起一次重排(重排会引起重绘)
参考文章
网页渲染性能优化(深)