提高前端性能:回流与重绘的优化策略
回流与重绘是什么
回流(reflow) 和 重绘(repaint) 是前端开发中性能优化的重要概念,它们对前端开发者来说非常重要,因为它们直接影响网页的性能和响应速度。
-
回流(reflow): 回流又称为重排,指的是当
页面布局
或DOM 元素的几何属性
(例如位置、大小)发生变化时,浏览器需要重新计算元素的几何属性,并重新布局整个页面。这是一个比较耗性能的操作,会导致页面重新渲染,因此应该尽量避免频繁的回流。 -
重绘(repaint): 指的是当
DOM 元素的样式属性
(例如颜色、背景)发生变化时,浏览器会重新绘制被影响的元素。重绘不会影响布局,只会重新绘制元素的外观,因此比回流的性能开销要小。
浏览器解析渲染机制
- 回流与重绘涉及到浏览器渲染机制,所以我们要通过浏览器渲染来深入了解回流与重绘。 浏览器解析渲染过程如图所示:
浏览器的渲染机制可以分为以下几个步骤:
-
构建DOM树:当浏览器接收到HTML文档时,它会将文档解析为一棵DOM树。DOM树是由节点(元素、文本等)及其关系组成的树状结构,表示了页面的结构和层次关系。
-
构建CSSOM树:同时,浏览器也会解析CSS样式表,构建CSSOM树(CSS Object Model)。CSSOM树表示了每个元素的样式信息,包括颜色、大小、边距等。
-
合并DOM树和CSSOM树:浏览器将DOM树和CSSOM树合并生成渲染树(Render Tree)。渲染树只包含需要显示的节点,即可见的网页内容。隐藏的元素(如display: none)不会包含在渲染树中。
-
布局(回流):渲染树中的每个节点都有其自己的盒子模型(包括尺寸、位置等)。浏览器会根据渲染树的每个节点计算其在屏幕上的准确位置和大小,这个过程称为布局或回流。
-
绘制(重绘):布局完成后,浏览器会将渲染树中的每个节点转换为屏幕上的实际像素,这个过程称为绘制或重绘。
-
合成与显示:最后,浏览器将渲染树的内容进行合成,并显示在屏幕上,呈现给用户。
回流与重绘触发机制
回流
- 以下操作会触发回流:
- 页面首次渲染(无法避免且开销最大的一次);
- 浏览器窗口大小发生改变(
resize
事件); - 添加或删除可见的 DOM 元素;
- 修改 DOM 元素的尺寸、位置、边距、填充等;
- 修改 DOM 元素的内容(文本、图片等);
- 激活CSS伪类,例如
:hover
、:active
、:focus
等; - 查询某些属性或调用某些方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect()
- 这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流。
重绘
- 下面是触发重绘的常见情况和相应的属性示例:
情况 | 属性示例 |
---|---|
修改元素的颜色属性 | color、background-color、border-color等 |
修改元素的文字属性 | font-weight、font-style、text-decoration等 |
修改元素的文本属性 | text-align、text-transform、line-height等 |
修改元素的背景属性 | background-image、background-position、background-size等 |
修改元素的盒子模型属性 | box-shadow、outline-color、outline-style等 |
使用渐变属性 | linear-gradient、radial-gradient等 |
使用变形属性 | transform、transform-origin等 |
使用过渡属性 | transition、transition-property、transition-duration等 |
这些属性的修改会引起元素外观的变化,从而触发重绘。
注意:重绘不一定导致回流,但回流一定会导致重绘。
浏览器的优化机制
- 由于每次回流与重绘都会带来额外的计算消耗,为了优化这个过程,大多数浏览器采用了队列化修改并批量执行的策略。浏览器会将修改操作添加到队列中,直至一定时间段过去或操作达到阈值时,才会清空队列。
- 然而,当需要获取布局信息时,浏览器会强制刷新队列。这意味着,当你读取元素的布局信息如
offsetTop
、offsetLeft
、getBoundingClientRect()
等时,需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流和重绘操作以返回正确的值。 - 建议在修改样式时尽量避免使用上述列出的属性,因为它们会导致渲染队列刷新。若确实需使用它们,最好将值缓存起来。
回流与重绘的优化策略
避免逐行修改样式,合并样式修改
- 以下操作会导致 3次重绘 1次回流:
const el = document.querySelector('.el');
el.style.color = 'blue'; // 导致重绘
el.style.backgroundColor = '#96f2d7'; // 导致重绘
el.style.margin = '10px'; // 导致回流(回流会引起重绘)
- 如果采用动态添加class或者使用cssText方式的话,只会导致1次回流,从而减少重绘次数:
.test {
color: blue;
background-color: #96f2d7;
margin: 10px;
}
const el = document.querySelector('.el');
el.classList.add('test')
//或者使用cssText
el.style.cssText = "color: blue; background-color: #96f2d7; margin: 10px;";
批量修改 DOM
DOM离线处理
- 离线的DOM不包含在当前DOM树中,因此对离线DOM的处理不会引起页面的回流和重绘。
- 使用
display: none
可以将元素从渲染树中彻底移除,元素既不可见,也不参与布局。在对该DOM进行操作时,不会触发回流和重绘,只有在操作完成后,将display
属性改为显示时,才会触发回流和重绘。 - 需要注意的是,
visibility: hidden
只会影响重绘,而不会影响重排。
- 使用
const el = document.querySelector('.el');
el.style.display = 'none';
//一系列修改样式、大小或添加删除子节点操作
el.style.display = 'block';
以上对隐藏的DOM元素操作不会引发其他元素的重排,只会在隐藏和显示时触发两次重排
文档片段(Document Fragment)
- 利用文档片段(Document Fragment) 在当前DOM之外构建一个子树,并将其复制回文档中。文档片段允许我们在内存中创建DOM结构,而不会直接影响到文档的回流和重绘。这样,在构建完整子树后,我们可以将文档片段的内容一次性地插入到文档中,这样只会触发一次重排。
const el = document.querySelector('.el');
const fragment = document.createDocumentFragment();
//批量添加子节点操作
el.appendChild(fragment);
拷贝替换
将原始元素复制到一个脱离文档的节点中,对该节点进行修改,然后再替换原始的元素。通过这种方式,我们可以在不影响主文档的情况下对元素进行操作,这样只会触发一次重排。
const el = document.querySelector('.el');
const clone = el.cloneNode(true);
//一系列修改样式、大小或添加删除子节点操作
el.parentNode.replaceChild(clone, ul);
使用 absolute 或 fixed 脱离文档流
-
当元素的
position
属性为absolute
或fixed
时,它们不会对其他元素的布局产生影响,因此在进行样式修改时,只有该元素本身及其子元素会触发重排和重绘。这样可以减小重排的范围,提高页面的渲染性能。 -
需要注意的是,将元素的
position
属性设置为absolute
或fixed
会使元素脱离文档流,可能会导致其他元素的布局错乱。在使用这种方法时,需要仔细考虑并进行适当的布局调整,以确保页面的正确显示和交互。
避免强制同步布局
在读取元素的布局信息(如offsetTop
、offsetLeft
、getBoundingClientRect()
等)时,会触发强制同步布局导致回流重绘。尽量避免频繁读取布局信息,可以通过缓存布局信息或一次性读取多个属性来减少回流和重绘。
使用节流和防抖
对于一些频繁触发的事件(如scroll
和resize
),可以使用节流(throttle) 或防抖(debounce) 来限制事件的触发频率,从而减少不必要的回流和重绘。
// 节流函数封装
function throttle(func, delay) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, arguments);
timer = null;
}, delay);
}
};
}
// 防抖函数封装
function debounce(func, delay) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
动态渲染少用 table 布局
在进行动态数据渲染时,建议尽量避免使用 table
布局。因为一旦 table
中的元素大小或内容发生改变,整个 table
都需要重新计算,这会引起不必要的回流和重绘操作。因此,我们可以尝试使用flex
、gird
等布局方式来避免这种情况的发生,从而提高页面的渲染效率。
使用硬件加速
使用 CSS3 动画代替JavaScript动画,transform
、opacity
、keyframes
和animation
等CSS属性来触发硬件加速,可以将动画效果交给 GPU 来处理,减少回流和重绘的开销,从而提高性能。
小结
以上我对浏览器渲染过程的回流重绘的理解,本人水平有限,如有错误欢迎在评论区指正,一起讨论!!ヾ(≧▽≦*)o
转载自:https://juejin.cn/post/7281581471897387071