高性能的DOM操作

1,799 阅读4分钟

ECMAScriptDOM 是两个不同的"小岛"在浏览器中, 每次互相的访问,都要收取"过路费", 因此我们要尽量减少过路费, 减少DOM与ECMAScript的交互

DOM访问和修改

1. 合并多次临时修改, 再最终一次统一修改dom

function innerHTMLLoop2(){
    let content = '';
    for(let i =0; i< 15000; i++){
        content += 'a';
    }
    document.getElementById('temp').innerHTML = content;
}

2. 使用 innerHTML 还是 DOM

大多数情况下 浏览器已经对 innerHTMLDOM 已经没有太大的效率区别 ,但是如果, 在一个对性能悠着苛刻要求的操作, 并更新一大段 HTML, 推荐使用innerHTML 方式

3. 节点克隆

elment.cloneNote() 替代 document.createElement() 在绝大多数的浏览器中,都比克隆节点有效率

4. HTML 集合

HTML集合一直与文档保持这连接, 每次我们访问这个HTML集合时, 都会重复执行查询过程, 哪怕只是获取集合里的元素个数(即访问集合的length属性)

优化:

  • 访问集合元素时候, 尽量使用局部变量缓存起来 (例如: 把集合的长度缓存到一个局部变量)
  • 先将集合元素拷贝到数组中, 那么访问它的属性会更快, 但是拷贝到数组会带来额外的销毁, 会多遍历一次集合 , 因此需要特定条件下评估

5. 遍历 DOM

  • childrenchildNodes中, childrenchildNodes 要快, 因为 HTML源码中的空白(文本节点)并不在children中, 但是会出现在childNodes
  • querySelectorAll() 这种 query开头的api 要比 document.getElement这种api要快, 尤其是,当我们需要 处理大量的 组合查询, 而且querySelector我们得到的是静态列表, 而 document.gelElement得到的是HTML集合

6.重绘与重排

  • DOM树:

DOM树中的每一个需要显示的节点, 在渲染树中至少存在一个对应的节点(隐藏DOM除外)

  • 渲染树:

渲染树中的节点被称为"帧(frame)或者盒(box)"

  • 总结:
  1. 当DOM上的变化引起了元素的几何物理,添加或删除DOM, 位置改变, 内容改变(被另一个不同尺寸的图片替换), 浏览器窗口发生改变,页面渲染器初始化, 那么会发生重排 + 重绘

  2. 根据改变的范围和程度, 渲染树会有对应部分的重新计算(重排), 但是也有整个页面的重排, 例如: 出现滚动条

  3. 如果知识改变颜色等,那么只有重绘没有重排

  4. 尽量避免使用:

    • offsetTop, offsetLeft, offsetWidth, offsetHeight
    • scrollTop, scrollLeft, scrollWidth, scrollHeight
    • clientTop, clientLeft, clientWidth, clientHeight
    • getComputedStyle() 因为浏览器通常会通过队列化修改和批量执行重排, 来优化浏览器对重排性能的优化. 但是, 这些属性都会强制刷新重排队列, 并要求立刻执行以保证获取最新的值. 因此我们应该在修改样式的工程中, 最好避免使用上面列出的属性. 因为他们都会刷新渲染队列, 即使你是在获取最近未发生改变的或者最新改变无关的布局信息.
  5. 最小化重绘和重排 利用cssText来代替 el.style.borderLeft=1px , 例如: el.style.cssText ="border-left:1px; border-right:2px; padding:5px"

  6. 批量修改DOM来减少重绘和重排

    思路: 元素脱离文档流 ==> 对其应用多重改变 ==> 把元素带回文档中

    1. (隐藏元素, 应用修改, 重新显示). 例如:
     ```js
    
          var ul = document.getElementById('mylist');
          ul.style.display = 'none';
          ul.appendChild(tempNode);
          ul.style.display = 'block';
     ```
     
    

    2. 使用文档片段 document.fragment

     ```js
             var fragment= document.createDocumentFragment();
             fragment.appendChild(tempNode);
             fragment.appendChild(temp2);
             var ul = document.getElementById('mylist');
             ul.appendChild(fragment);
     ```
    

    3. 把原始元素拷贝到一个脱离文档的节点中, 对副本进行操作, 完成后替换原始元素 js var old = document.getElementById('mylist'); var clone = old.cloneNode(true); clone.appendChild(temp); old.parentNode.replaceChild(clone, old);

  7. 缓存布局信息

// bad
myElement.style.left = 1 + myElement.offsetLeft + 'px'; // 每次都去读与写, 读取的时候回有强制刷新
myElement.style.top = 1 + myElement.offsetTop + 'px';
if(myElement.offsetLeft >=500){
    stopAnimation();
}

// good
var current = myElement.offsetLeft; // 只读一次
current++;
myElement.style.left = current + 'px'; // 只有写,没有读, 不在查询, 因此效率大大提升
myElement.style.top = current + 'px';
if(current>=500){
    stopAnimation();
}

  1. 动画过程中, 让目标元素脱离文档流, 减小重排范围
  • 利用绝对定位

7. 较少DOM上的事件绑定

大量的DOM上的事件绑定会严重损耗我们的DOM性能 经典案例: ul上绑定事件监听, 利用冒泡机制, 从而监听我们的 li, 而不是在100个甚至更多的li上 逐一绑定监听事件

总结

访问和操作DOM是现代浏览器的重要部分

  • 最小化DOM访问次数, 尽可能在js端处理
  • 如果要多次访问DOM节点, 请用局部变量储存它
  • 使用速度更快的API, 例如 querySelectorAllfirstElementChild
  • 尽量一次性批量修改样式, 和 "离线"操作DOM树, 使用缓存, 并减少访问布局信息的次数
  • 动画中使用绝对定位
  • 使用事件委托减少事件处理器的数量