当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
引发回流的操作:
1.修改dom元素。当一个DOM元素的几何属性发生变化时,所有和它相关的节点(比如父子节点、兄弟节点等)的几何属性都需要进行重新计算。
2.页面首次渲染
3.浏览器窗口大小发生改变
4.元素的尺寸和位置发生改变(width、height、padding、margin、left、top、border )等等
当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
一句话:回流必将引起重绘,重绘不一定会引起回流
如果每次 DOM 操作都即时地反馈一次回流或重绘,那么性能上来说是扛不住的。所以很多浏览器都会优化这些操作,于是它自己缓存了一个 flush 队列,把所有会引起回流、重绘的操作放入这个队列,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。 这样就会让多次的回流、重绘变成一次回流重绘。
注意: 队列强制刷新
因为有的时候我们需要精确获取某些样式信息,例如:
offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop/Left/Width/Height clientTop/Left/Width/Height width,height 请求了getComputedStyle(), 或者 IE的 currentStyle 这个时候,浏览器为了反馈最精确的信息,需要立即回流重绘一次,确保给到我们的信息是准确的,所以可能导致 flush 队列提前执行了。
css:
1、避免使用table布局。 2、尽可能在DOM树的最末端改变class。 3、避免设置多层内联样式。 4、将动画效果应用到position属性为absolute或fixed的元素上。 5、避免使用CSS表达式(例如:calc())。
js:
1、避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。 2、避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。 3、也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。 4、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 5、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
有时我们想要通过多次计算得到一个元素的布局位置,我们可能会这样做:
Document
这样做,每次循环都需要获取多次“敏感属性”,是比较糟糕的。我们可以将其以 JS 变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求:
// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el')
let offLeft = el.offsetLeft, offTop = el.offsetTop
// 在JS层面进行计算
for(let i=0;i<10;i++) {
offLeft += 10
offTop += 10
}
// 一次性将计算结果应用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop + "px"
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
每次单独操作,都去触发一次渲染树更改,从而导致相应的回流与重绘过程。
优化成一个有 class 加持的样子:
Document
const container = document.getElementById('container')
container.style.cssText += 'width: 100px; height: 200px; border: 10px solid red; color: red;';
合并之后,等于我们将所有的更改一次性发出,用一个 style 请求解决掉了。
我们给元素设置 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');
appendDataToElement(ul, data);
如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。
优化操作:隐藏元素,应用修改,重新显示
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';
当我们进行少量 DOM 操作时,DOM 离线化的优越性确实不太明显。一旦操作频繁起来,这“拿掉”和“放回”的开销都将会是非常值得的。
参考:
https://juejin.im/post/6844903987091603463#heading-4
https://juejin.im/post/6844903942137053192#heading-6
https://juejin.im/post/6844903656836317198#heading-6