JavaScript内核与高级编程之:`JavaScript`的`DOM`优化:如何减少回流(`reflow`)和重绘(`repaint`)。

各位前端er们,晚上好!我是老码,今天咱们来聊聊JavaScript的DOM优化,重点是如何减少回流(reflow)和重绘(repaint)。这俩兄弟可是性能大户,一不小心就会让你的页面卡成PPT。所以,优化它们,刻不容缓!

一、什么是回流(Reflow)和重绘(Repaint)?

想象一下,你装修房子,改了墙的颜色,这叫重绘。但如果你把墙拆了重新砌,那可是回流了!

  • 重绘(Repaint): 元素样式的改变并不影响它在文档流中的位置(例如:colorbackground-colorvisibility),浏览器只需重新绘制受影响的部分。简单来说,就是换个颜色,刷个漆。

  • 回流(Reflow): 元素的尺寸、布局、或内容发生改变,导致文档流重新计算,影响到其他元素的布局,浏览器需要重新构建渲染树。这可是个大工程,类似于推倒重来,性能消耗巨大。常见的触发回流的操作包括:

    • 改变窗口大小(resize)
    • 改变字体(font-size)
    • 添加或删除可见的DOM元素
    • 改变元素的位置(position)
    • 改变元素的尺寸(margin、padding、border、width、height)
    • 内容改变,例如输入框输入文字
    • 读取某些DOM属性(offsetLeft、offsetTop、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle()等等)

记住,回流必定会引起重绘,而重绘不一定会引起回流。回流就像多米诺骨牌,一个倒下,一片跟着倒。所以,我们的目标是尽量避免回流!

二、回流和重绘的代价

每次回流,浏览器都要重新计算整个页面或者部分页面的几何信息,这非常消耗资源。重绘虽然代价小一些,但频繁的重绘也会影响用户体验。如果你的页面动画不流畅,或者响应迟缓,很可能就是回流和重绘在作祟。

三、如何减少回流和重绘?

现在,进入正题,也是大家最关心的部分:如何减少回流和重绘?记住,我们的目标是:减少不必要的回流,让浏览器更轻松地工作。

1. 集中修改样式

避免频繁地直接修改DOM元素的样式,最好一次性修改。

  • 错误示范:

    let element = document.getElementById('myElement');
    element.style.width = '100px'; // 回流
    element.style.height = '200px'; // 回流
    element.style.backgroundColor = 'red'; // 重绘

    每次修改样式都会触发回流或重绘,性能很差。

  • 正确示范:

    • 使用CSS类名: 先定义好CSS类,然后通过修改元素的类名来批量修改样式。

      // CSS
      .newStyle {
          width: 100px;
          height: 200px;
          background-color: red;
      }
      
      // JavaScript
      element.classList.add('newStyle'); // 只回流一次
    • 使用CSS文本: 直接修改style.cssText属性,一次性设置所有样式。

      element.style.cssText = 'width: 100px; height: 200px; background-color: red;'; // 只回流一次
    • 使用setProperty:

        element.style.setProperty('width', '100px');
        element.style.setProperty('height', '200px');
        element.style.setProperty('backgroundColor', 'red');

2. 批量修改DOM

避免频繁地添加、删除DOM元素,尽量批量操作。

  • 错误示范:

    let container = document.getElementById('myContainer');
    for (let i = 0; i < 100; i++) {
        let newElement = document.createElement('div');
        newElement.textContent = 'Item ' + i;
        container.appendChild(newElement); // 每次appendChild都会触发回流
    }
  • 正确示范:

    • 使用DocumentFragment: 先将所有DOM操作放到DocumentFragment中,然后一次性添加到页面。DocumentFragment 是一个轻量级的 Document 对象,它存在于内存中,不属于文档树,因此对其进行操作不会触发回流和重绘。

      let container = document.getElementById('myContainer');
      let fragment = document.createDocumentFragment(); // 创建文档片段
      for (let i = 0; i < 100; i++) {
          let newElement = document.createElement('div');
          newElement.textContent = 'Item ' + i;
          fragment.appendChild(newElement); // 将元素添加到文档片段
      }
      container.appendChild(fragment); // 一次性添加到页面,只回流一次
    • 使用innerHTML(谨慎): 虽然innerHTML简单粗暴,但如果只是添加简单的文本内容,性能还不错。但是,对于复杂的HTML结构,还是建议使用DocumentFragment,因为innerHTML会重新解析HTML字符串,可能会有安全风险。

      let container = document.getElementById('myContainer');
      let html = '';
      for (let i = 0; i < 100; i++) {
          html += '<div>Item ' + i + '</div>';
      }
      container.innerHTML = html; // 可能会回流,取决于浏览器优化

3. 缓存DOM信息

避免频繁读取DOM元素的属性,例如offsetWidthoffsetHeight等。每次读取这些属性,浏览器为了保证数据的准确性,都会强制进行回流。

  • 错误示范:

    let element = document.getElementById('myElement');
    for (let i = 0; i < 100; i++) {
        let width = element.offsetWidth; // 每次循环都触发回流
        // ...其他操作
    }
  • 正确示范:

    let element = document.getElementById('myElement');
    let width = element.offsetWidth; // 只读取一次
    for (let i = 0; i < 100; i++) {
        // 使用缓存的width
        // ...其他操作
    }

4. 离线修改DOM

如果需要对DOM元素进行大量的修改,可以先将元素从文档流中移除,修改完成后再放回去。

  • 方法一:display: none

    先将元素设置为display: none,这样对元素的操作不会触发回流,修改完成后再设置为display: block

    let element = document.getElementById('myElement');
    element.style.display = 'none'; // 移除文档流
    // ...进行大量修改
    element.style.display = 'block'; // 恢复文档流,触发一次回流
  • 方法二:使用cloneNode

    克隆一个节点,修改克隆节点,然后用克隆节点替换原始节点。

    let element = document.getElementById('myElement');
    let clone = element.cloneNode(true); // 深拷贝
    // ...对clone进行大量修改
    element.parentNode.replaceChild(clone, element); // 替换原始节点,触发一次回流

5. 避免使用Table布局

Table布局非常容易触发回流,因为Table中一个元素的改变可能会影响到整个Table的布局。尽量使用CSS布局(Flexbox、Grid)代替Table布局。

6. 避免使用CSS表达式(Expression)

CSS表达式在每次页面重绘时都会重新计算,性能很差,尽量避免使用。

7. 优化动画

动画是回流和重绘的重灾区,优化动画至关重要。

  • 使用requestAnimationFrame 使用requestAnimationFrame来执行动画,可以让浏览器在最佳的时机执行动画,避免掉帧。

    function animate() {
        // ...动画逻辑
        requestAnimationFrame(animate);
    }
    requestAnimationFrame(animate);
  • 使用transformopacity 尽量使用transformopacity来实现动画,因为它们不会触发回流,只会触发重绘。

    element.style.transform = 'translate(100px, 100px)'; // 只触发重绘
    element.style.opacity = 0.5; // 只触发重绘

    为什么transformopacity性能更好?

    因为transformopacity的改变通常由GPU处理,而不是CPU。GPU更擅长处理图形渲染,因此性能更好。

8. 合理使用CSS选择器

CSS选择器的效率也会影响性能。尽量使用ID选择器和类选择器,避免使用复杂的选择器,例如通配符选择器(*)和属性选择器([attribute])。

9. 避免频繁的DOM操作

这是最重要的一点。任何DOM操作都可能触发回流和重绘,所以尽量减少DOM操作。

10. 利用Shadow DOM

Shadow DOM 可以将一个 DOM 节点与其子节点隔离,从而避免了全局 CSS 样式对这些节点的影响。这意味着,当 Shadow DOM 中的元素发生变化时,不会影响到页面上的其他元素,从而减少了回流和重绘的范围。

11. 使用Web Workers

Web Workers 允许你在后台线程中执行 JavaScript 代码,而不会阻塞主线程。你可以将一些耗时的 DOM 操作放到 Web Workers 中执行,从而避免了主线程的卡顿。

总结:减少回流和重绘的黄金法则

法则 描述 示例
集中修改样式 将多个样式修改合并为一个操作。 使用CSS类名、cssTextsetProperty
批量修改DOM 使用DocumentFragmentinnerHTML批量添加DOM元素。 创建DocumentFragment,将多个元素添加到DocumentFragment,然后一次性添加到DOM。
缓存DOM信息 避免重复读取DOM属性,将结果缓存起来。 offsetWidthoffsetHeight等属性缓存到变量中。
离线修改DOM 先将元素从文档流中移除,修改完成后再放回去。 使用display: none或克隆节点。
避免Table布局 尽量使用CSS布局代替Table布局。 使用Flexbox或Grid布局。
避免CSS表达式 避免使用CSS表达式。 使用JavaScript代替CSS表达式。
优化动画 使用requestAnimationFrametransformopacity 使用requestAnimationFrame执行动画,使用transform进行位移、旋转、缩放,使用opacity改变透明度。
合理使用CSS选择器 使用ID选择器和类选择器,避免使用复杂的选择器。 尽量避免使用通配符选择器和属性选择器。
减少DOM操作 这是最重要的一点。 尽量避免不必要的DOM操作。
利用Shadow DOM 将 DOM 节点与其子节点隔离,避免全局 CSS 样式对这些节点的影响。 使用 Shadow DOM 创建独立的组件。
使用Web Workers 将耗时的 DOM 操作放到 Web Workers 中执行,避免阻塞主线程。 将数据处理和复杂的计算放到 Web Workers 中。

四、工具与技巧

  • 浏览器开发者工具: 现代浏览器都提供了强大的开发者工具,可以帮助你分析页面的性能瓶颈。通过Timeline或Performance面板,你可以清晰地看到回流和重绘的发生情况,以及它们所消耗的时间。

  • Lighthouse: Google的Lighthouse是一个开源的自动化工具,可以用来审计网页的性能、可访问性、最佳实践等。它可以帮助你发现潜在的性能问题,并提供优化建议。

  • Performance API: JavaScript提供了Performance API,可以让你更精确地测量页面的性能指标,例如回流和重绘的时间。

五、案例分析

假设我们有一个需求:实现一个列表的动态排序。

  • 传统方法(容易触发回流):

    每次点击排序按钮,都重新生成整个列表的DOM结构。

  • 优化方法:

    1. 先对数据进行排序。
    2. 使用DocumentFragment批量更新列表的DOM结构。

通过这种方式,可以大大减少回流的次数,提高性能。

六、总结

减少回流和重绘是一个持续优化的过程,没有一劳永逸的解决方案。我们需要根据具体的场景,选择合适的优化策略。记住,代码优化没有银弹,只有不断学习和实践,才能写出高性能的JavaScript代码。

希望今天的分享对大家有所帮助!记住,优化性能,就是优化用户体验! 感谢大家! 下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注