回流与重绘
在讨论“回流和重绘”之前,我们需要先了解一下浏览器的渲染流程:
- 解析 HTML,生成 DOM 树
- 解析 CSS,生成 CSSOM 树
- 合并 DOM 树和 CSSOM 树,去掉不可见元素,生成渲染树(Render Tree)
- Layout(布局):根据生成的渲染树,进行布局,计算得到节点的几何信息(位置,大小)
- Painting(绘制):根据渲染树以及节点的几何信息,将渲染树的每个像素渲染到屏幕上
为了构建渲染树,浏览器主要完成了以下工作:
从 DOM 树的根节点开始遍历每个可见节点
- 某些节点不会被渲染输出,比如
script
、link
、meta
等标签 - 通过 CSS 隐藏的节点会被忽略,比如
display: none;
。但是通过visibility: hidden;
隐藏的节点还是会被渲染的,因为它仍占据布局空间
- 某些节点不会被渲染输出,比如
对于每个可见节点,找到 CSSDOM 树中对应的规则,并应用它们
根据每个可见节点以及对应的样式,组合生成渲染树
回流(reflow)
当渲染树中的节点的尺寸,边距等信息发生改变时,就需要重新计算各节点的几何信息,这个过程就叫做回流。
会触发回流的操作:
盒模型相关属性变化:
width
、height
、margin
、display
、border
等定位属性以及浮动等相关属性变化:
top
、position
、float
等节点内部文字结构发生变化:
text-align
、overflow
、font-size
、line-height
、vertical-align
等页面首次渲染
添加或删除可见的 DOM 元素,进行 DOM 操作
节点中的文本或图片尺寸变化
浏览器的窗口尺寸变化
CSS 伪类激活,例如
:hover
获取布局信息的操作:
offsetWidth
、offsetHeight
、clientWidth
、clientHeight
、width
、height
、scrollTop
、scrollHeight
,getComputedStyle()
,getBoundingClientRect()
等
重绘(repaint)
当元素的外观发生改变时,例如visibility
、color
、background-color
等属性的改变,不会影响到布局。浏览器会根据元素的新属性重新绘制,是元素呈现新的外观,这个过程叫做重绘。
根据浏览器的渲染流程可知:回流一定会触发重绘,但重绘不一定会回流
性能优化
回流和重绘的代价是很昂贵的,因此要尽可能减少回流和重绘发生的次数。
合并多次样式修改
合并多次样式的修改,然后一次性处理掉,例如:
const box = document.querySelector(".box");
box.style.width = "50px";
box.style.height = "50px";
box.style.border = "1px solid #000"
上面的例子中三个样式属性被改变,会触发三次回流。我们可以合并这三次操作,然后统一处理:
const box = document.querySelector(".box");
box.style.cssText = "width: 50px;height: 50px;border: 1px solid #000;";
最好的方式是给这几个样式定义一个类名,然后给元素添加这个类名:
const box = document.querySelector(".box");
box.classList.add("whb");
批量操作DOM
下面的例子中每次给别添加一个li
,都会触发一次回流:
const arr = ["html", "css", "javascript"];
const list = document.querySelector("#list");
for(let i = 0; i < arr.length; i++) {
let li = document.createElement("li");
li.innerText = arr[i];
list.appendChild(li);
}
可以使用documentFragment
文档片段,先把所有的li
插入到文档片段中,再将文档片段插入ul
中:
const arr = ["html", "css", "javascript"];
const list = document.querySelector("#list");
const fragment = document.createDocumentFragment();
for(let i = 0; i < arr.length; i++) {
let li = document.createElement("li");
li.innerText = arr[i];
fragment.appendChild(li);
}
list.appendChild(fragment);
减少或避免强制同步布局
在访问某些属性(offsetWidth
那一堆属性)时,会导致浏览器强制清空队列,进行强制同步布局。实际使用中应该尽量减少或避免。
比如我们想批量设置一些元素的宽度为某个 box 的宽度:
for(let i = 0; i < elements.length; i++) {
elements[i].style.width = box.offsetWidth + "px";
}
我们可以把读取到的offsetWidth
属性值进行缓存:
const boxWidth = box.offsetWidth;
for(let i = 0; i < elements.length; i++) {
elements[i].style.width = boxWidth + "px";
}
使用transform
和opacity
来实现动画
比如使用translate
代替left
、top
。使用opacity
代替visibility
等。
动画效果应用在 position 属性为 absolute 或 fixed 的元素上
使用绝对定位,避免父元素以及后续元素的频繁回流。
尽可能在 DOM 树的最末端改变 class
在最末端改变class,可以使其影响尽可能少的节点。
避免使用 table 布局
浏览器使用的是流式布局模型,对于渲染树的计算通常只需要遍历一次就可以完成,但table以及内部的元素可能需要多次计算。
参考资料: