前端的重绘重排

06-01 1291阅读

网页生成过程

网页的生成是一个复杂的过程,主要包括以下几个阶段:

  1. HTML解析阶段:
    • 浏览器逐行解析HTML文档。
    • 构建DOM(Document Object Model)树。
    • 遇到标签会暂停解析,执行脚本(除非使用async/defer)。
    • CSS解析阶段:
      • 解析外部CSS文件和内联样式。
      • 构建CSSOM(CSS Object Model)树。
      • 解析过程是递归的(从右向左选择器匹配)。
      • 渲染树构建:
        • 合并DOM树和CSSOM树。
        • 只包含需要显示的节点(如不包含display:none的元素)。
        • 计算每个节点的CSS属性值。
        • 布局阶段(Layout/Reflow):
          • 计算每个节点在视口中的确切位置和大小。
          • 从根节点开始递归计算。
          • 生成"盒模型"精确信息。
          • 绘制阶段(Paint):
            • 将布局计算的几何信息转换为屏幕上的实际像素。
            • 通常分为多个图层(Composite Layers)进行绘制。
            • 最后进行图层合成(Compositing)。


  网页生成示例
  
    /* 定义一个盒子样式 */
   .box {
      width: 200px;
      height: 200px;
      background-color: lightblue;
      margin: 20px;
    }
  


  
  
// 打印信息,表明DOM内容已加载 console.log('DOMContentLoaded'); // 获取元素并修改其样式 const box = document.querySelector('.box'); box.style.border = '2px solid red';

重绘(Repaint)

重绘是指视觉外观改变但是几何位置不变的时候,浏览器需要重新绘制元素外观的过程。

举例

颜色、背景、阴影、文字内容等不会影响元素布局的内容的改变会触发重绘。

触发条件

  • 修改元素的 color、background-color、border-style、box-shadow 等样式属性。
  • 修改 visibility(不影响布局,但会改变视觉状态)。
  • 修改文本内容(如 textContent)。
  • 滚动条样式的变化(如 scrollbar-width)。
    
    
      
      

    改变颜色触发重绘

    改变颜色
    改变背景触发重绘
    改变背景 改变文本内容触发重绘 改变文本 function changeColor() { const p = document.getElementById('color-change'); p.style.color = 'blue'; } function changeBackground() { const div = document.getElementById('bg-change'); div.style.backgroundColor = 'orange'; } function changeText() { const span = document.getElementById('text-change'); span.textContent = '文本已改变'; }

    重排(Reflow)

    重排是指几何位置或者尺寸发生改变的时候就会触发重排,更新页面布局的行为。

    举例

    元素宽度、高度、边距、定位、添加或删除DOM节点等操作会触发重排。

    触发条件

    • 几何属性修改:width、height、padding、margin、border-width、top、left、transform(2D/3D 转换会触发合成,但若涉及布局变化也会触发重排)等。
    • 布局相关操作:
      • 添加或删除可见的 DOM 节点、修改 display(如 none 会触发重排,visibility: hidden 仅触发重绘)、flex/grid 布局的变化。
      • 获取布局信息(浏览器会强制同步布局计算):offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、clientWidth、clientHeight、getBoundingClientRect() 等属性或方法的调用。

        特点

        重排成本很高,会影响整个界面布局,导致父子元素连锁变化。浏览器尽可能批量处理重排,但是频繁重排会导致性能瓶颈。

        以下是多个触发重排的代码示例:

        
        
          
          
        改变宽度触发重排
        改变宽度
        添加节点触发重排
        添加节点
        修改display触发重排
        修改display function changeWidth() { const div = document.getElementById('width-change'); div.style.width = '200px'; } function addNode() { const div = document.getElementById('node-add'); const newDiv = document.createElement('div'); newDiv.textContent = '新添加的节点'; div.appendChild(newDiv); } function changeDisplay() { const div = document.getElementById('display-change'); div.style.display = 'none'; }

        重绘与重排的关系

        重排通常伴随着重绘,但是重绘不一定有重排。

        性能优化

        为了提高网页性能,我们需要尽量减少重绘和重排的次数。以下是一些性能优化的方法:

        1. 减少重排重绘次数
          • 批量修改:
        // 不好的写法:多次修改样式(触发多次重排)
        const element = document.createElement('div');
        element.style.width = '100px';
        element.style.height = '200px';
        element.style.backgroundColor = 'red';
        // 好的写法:通过 class 一次性修改
        const newElement = document.createElement('div');
        newElement.className = 'new-style';
        
        .new-style {
          width: 100px;
          height: 200px;
          background-color: red;
        }
        
        - **使用文档碎片**:
        

        批量操作 DOM 时,先将节点添加到 DocumentFragment 中,再一次性插入 DOM 树,避免多次触发重排。

        const fragment = document.createDocumentFragment();
        for (let i = 0; i  
        
        1. 避免频繁的获取布局信息
          • 缓存布局信息:如需多次读取元素的布局属性(如 offsetWidth),先将其值缓存到变量中,避免重复触发重排。
        // 不好的写法:多次读取 offsetWidth(触发多次重排)
        const badElement = document.createElement('div');
        for (let i = 0; i  
        
        1. 利用层叠上下文(CSS 3D/硬文件加速)

          将频繁动画的元素脱离文档流,使用 will-change 或 transform: translateZ(0) 使其创建层叠上下文,让浏览器为其单独分配图层,避免影响其他元素的布局。

        
        
          
           .animated-element {
              width: 100px;
              height: 100px;
              background-color: lightblue;
              will-change: transform;
              transform: translateZ(0);
              animation: move 2s infinite;
            }
            @keyframes move {
              from {
                transform: translateX(0);
              }
              to {
                transform: translateX(200px);
              }
            }
          
        
        
          
        1. 合理使用display:none

          对需要频繁操作的元素,先设置 display: none(使其脱离文档流,重排成本降低),操作完成后再显示。

        const element = document.createElement('div');
        element.style.display = 'none'; // 触发一次重排
        // 模拟执行大量 DOM 修改
        for (let i = 0; i  
        
        1. 使用 CSS 动画替代 JavaScript 动画

          CSS 动画(如 transition、animation)由浏览器优化处理,尽量避免通过 JavaScript 频繁修改样式触发重排。

        
        
          
           .css-animation {
              width: 100px;
              height: 100px;
              background-color: lightgreen;
              transition: width 1s;
            }
           .css-animation:hover {
              width: 200px;
            }
          
        
        
          
        1. 集中修改样式
        
        
          
        不好的写法 好的写法 .new-style { left: 10px; top: 200px; transform: scale(1.1); } const box = document.getElementById('box'); function badChange() { box.style.left = '10px'; box.style.top = '200px'; box.style.transform = 'scale(1.1)'; } function goodChange() { box.classList.add('new-style'); }
        1. 不使用table布局

          table中任何元素变化都会导致整个表格重排,因此尽量避免使用。以下是一个简单的表格示例及对比:

        
        
          
          
        单元格1 单元格2
        改变表格
        单元格1
        单元格2
        改变 div .div-layout { display: flex; } .div-cell { border: 1px solid black; padding: 5px; } function changeTable() { const table = document.querySelector('table'); const newCell = document.createElement('td'); newCell.textContent = '新单元格'; const row = table.rows[0]; row.appendChild(newCell); } function changeDiv() { const divLayout = document.querySelector('.div-layout'); const newDiv = document.createElement('div'); newDiv.textContent = '新 div'; newDiv.classList.add('div-cell'); divLayout.appendChild(newDiv); }
        1. 离线修改DOM
          • DOM设置display: none;,将其从渲染树中移除,然后再进行复杂修改操作,最后再将其显示出来。整个过程包含隐藏和显示共两次重排。
          • 或者使用DocumentFragment创建一个DOM碎片,然后在其上面批量操作DOM,操作完成之后再添加到文档中,这样只会触发一次重排。
        // 方法一:使用 display: none
        const element = document.createElement('div');
        document.body.appendChild(element);
        element.style.display = 'none';
        for (let i = 0; i  
        
        1. 分离读写操作
        
        
          
        不好的读写操作 好的读写操作 const div = document.getElementById('div'); function badReadWrite() { div.style.left = '10px'; div.style.top = '10px'; div.style.width = '20px'; div.style.height = '20px'; console.log(div.offsetLeft); console.log(div.offsetTop); console.log(div.offsetWidth); console.log(div.offsetHeight); } function goodReadWrite() { const width = div.offsetWidth; const height = div.offsetHeight; div.style.width = (width + 10) + 'px'; div.style.height = (height + 10) + 'px'; console.log(width); console.log(height); }

        "统一读,再统一写"的优化原理,实际上是浏览器渲染机制中的**布局抖动(Layout Thrashing)**问题的解决方案。以下是详细解释:

        前端的重绘重排
        (图片来源网络,侵删)

        核心机制解析

        1. 浏览器渲染队列优化:

          现代浏览器会维护一个"写操作队列",将连续的样式修改操作批量处理。这种批处理可以避免每次样式修改都立即触发重排。

        2. 强制同步布局(Forced Synchronous Layout):

          当JavaScript代码读取布局属性(如offsetWidth、clientHeight等)时,浏览器必须立即计算最新布局以保证返回准确值,这会强制刷新并执行当前队列中的所有待处理写操作。

        3. 典型问题场景:
          // 反模式:读写交替
          const element = document.createElement('div');
          element.style.width = '100px';  // 写 - 加入队列
          let w1 = element.offsetWidth;   // 读 - 强制刷新队列(触发重排)
          element.style.height = '200px'; // 写 - 加入队列
          let h1 = element.offsetHeight;  // 读 - 再次强制刷新队列(触发重排)
        

        这样会导致多次不必要的重排。

        前端的重绘重排
        (图片来源网络,侵删)

        统一读写法示例

        const element = document.createElement('div');
        // 统一读写法示例
        const width = element.offsetWidth;  // 第一次读取(触发1次重排)
        console.log(width);                // 仅仅是日志输出,不涉及布局读取
        console.log(width);                // 同上
        
        1. 第一次读取:

          element.offsetWidth强制浏览器执行队列中的所有待处理操作,触发1次完整的重排计算,并将结果缓存到width变量。

        2. 后续console.log:

          只是输出已缓存的变量值,不涉及任何布局属性读取,不会触发额外的重排。

        为什么不会多次触发重排?

        关键区别在于:

        前端的重绘重排
        (图片来源网络,侵删)
        • 读取布局属性:offsetWidth等会强制重排。
        • 普通变量读取:已缓存的变量不会触发重排。

          浏览器的工作流程:

          [初始状态] 写队列: []
          1. 读取offsetWidth:
             - 发现需要最新布局信息
             - 立即执行: 刷新写队列 → 计算布局 → 返回结果
             - 重排计数 +1
          2. 后续console.log:
             - 只是JavaScript执行环境中的变量访问
             - 不涉及渲染引擎
             - 不会触发重排
          

          实际开发中的正确实践

          const element = document.createElement('div');
          // 1. 先批量读取所有需要的布局信息
          const width = element.offsetWidth;
          const height = element.offsetHeight;
          // 2. 然后执行批量样式修改
          element.style.width = (width + 10) + 'px';
          element.style.height = (height + 10) + 'px';
          // 整个过程只触发1次初始读取时的重排
          

          特殊情况的注意事项

          即使统一读取,某些情况下仍可能触发多次重排:

          const element1 = document.createElement('div');
          const element2 = document.createElement('div');
          const width1 = element1.offsetWidth; // 重排1
          const width2 = element2.offsetWidth; // 可能再次重排
          

          因为不同元素的布局计算可能有依赖关系,浏览器可能需要多次计算。最安全的做法是使用requestAnimationFrame来分离读写操作。

          这种优化原理正是Facebook的React等框架实现虚拟DOM(Virtual DOM)批处理更新的核心机制之一。


免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码