likes
comments
collection
share

提高前端性能:回流与重绘的优化策略

作者站长头像
站长
· 阅读数 28

回流与重绘是什么

回流(reflow)重绘(repaint) 是前端开发中性能优化的重要概念,它们对前端开发者来说非常重要,因为它们直接影响网页的性能和响应速度。

  • 回流(reflow): 回流又称为重排,指的是当页面布局DOM 元素的几何属性(例如位置、大小)发生变化时,浏览器需要重新计算元素的几何属性,并重新布局整个页面。这是一个比较耗性能的操作,会导致页面重新渲染,因此应该尽量避免频繁的回流。

  • 重绘(repaint): 指的是当 DOM 元素的样式属性(例如颜色、背景)发生变化时,浏览器会重新绘制被影响的元素。重绘不会影响布局,只会重新绘制元素的外观,因此比回流的性能开销要小。

浏览器解析渲染机制

  • 回流与重绘涉及到浏览器渲染机制,所以我们要通过浏览器渲染来深入了解回流与重绘。 浏览器解析渲染过程如图所示:

提高前端性能:回流与重绘的优化策略

浏览器的渲染机制可以分为以下几个步骤:

  1. 构建DOM树:当浏览器接收到HTML文档时,它会将文档解析为一棵DOM树。DOM树是由节点(元素、文本等)及其关系组成的树状结构,表示了页面的结构和层次关系。

  2. 构建CSSOM树:同时,浏览器也会解析CSS样式表,构建CSSOM树(CSS Object Model)。CSSOM树表示了每个元素的样式信息,包括颜色、大小、边距等。

  3. 合并DOM树和CSSOM树:浏览器将DOM树和CSSOM树合并生成渲染树(Render Tree)。渲染树只包含需要显示的节点,即可见的网页内容。隐藏的元素(如display: none)不会包含在渲染树中。

  4. 布局(回流):渲染树中的每个节点都有其自己的盒子模型(包括尺寸、位置等)。浏览器会根据渲染树的每个节点计算其在屏幕上的准确位置和大小,这个过程称为布局或回流。

  5. 绘制(重绘):布局完成后,浏览器会将渲染树中的每个节点转换为屏幕上的实际像素,这个过程称为绘制或重绘。

  6. 合成与显示:最后,浏览器将渲染树的内容进行合成,并显示在屏幕上,呈现给用户。

回流与重绘触发机制

回流

  • 以下操作会触发回流:
  1. 页面首次渲染(无法避免且开销最大的一次);
  2. 浏览器窗口大小发生改变(resize事件);
  3. 添加或删除可见的 DOM 元素;
  4. 修改 DOM 元素的尺寸、位置、边距、填充等;
  5. 修改 DOM 元素的内容(文本、图片等);
  6. 激活CSS伪类,例如 :hover:active:focus等;
  7. 查询某些属性或调用某些方法:
    • 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等

这些属性的修改会引起元素外观的变化,从而触发重绘。

注意重绘不一定导致回流,但回流一定会导致重绘。

浏览器的优化机制

  • 由于每次回流与重绘都会带来额外的计算消耗,为了优化这个过程,大多数浏览器采用了队列化修改并批量执行的策略。浏览器会将修改操作添加到队列中,直至一定时间段过去或操作达到阈值时,才会清空队列。
  • 然而,当需要获取布局信息时,浏览器会强制刷新队列。这意味着,当你读取元素的布局信息如offsetTopoffsetLeftgetBoundingClientRect()等时,需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流和重绘操作以返回正确的值。
  • 建议在修改样式时尽量避免使用上述列出的属性,因为它们会导致渲染队列刷新。若确实需使用它们,最好将值缓存起来。

回流与重绘的优化策略

避免逐行修改样式,合并样式修改

  • 以下操作会导致 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属性为absolutefixed时,它们不会对其他元素的布局产生影响,因此在进行样式修改时,只有该元素本身及其子元素会触发重排和重绘。这样可以减小重排的范围,提高页面的渲染性能。

  • 需要注意的是,将元素的position属性设置为absolutefixed会使元素脱离文档流,可能会导致其他元素的布局错乱。在使用这种方法时,需要仔细考虑并进行适当的布局调整,以确保页面的正确显示和交互。

避免强制同步布局

在读取元素的布局信息(如offsetTopoffsetLeftgetBoundingClientRect()等)时,会触发强制同步布局导致回流重绘。尽量避免频繁读取布局信息,可以通过缓存布局信息或一次性读取多个属性来减少回流和重绘。

使用节流和防抖

对于一些频繁触发的事件(如scrollresize),可以使用节流(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 都需要重新计算,这会引起不必要的回流和重绘操作。因此,我们可以尝试使用flexgird等布局方式来避免这种情况的发生,从而提高页面的渲染效率。

使用硬件加速

使用 CSS3 动画代替JavaScript动画,transformopacitykeyframesanimation等CSS属性来触发硬件加速,可以将动画效果交给 GPU 来处理,减少回流和重绘的开销,从而提高性能。

小结

以上我对浏览器渲染过程的回流重绘的理解,本人水平有限,如有错误欢迎在评论区指正,一起讨论!!ヾ(≧▽≦*)o