前端性能优化--为什么DOM操作慢? 浅谈DOM的操作以及性能优化问题-重绘重排 为什么要减少DOM操作 为什么要减少操作DOM

时间:2023-03-09 02:24:32
前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM

前端性能优化--为什么DOM操作慢?

作为一个前端,不能不考虑性能问题。对于大多数前端来说,性能优化的方法可能包括以下这些:

  1. 减少HTTP请求(合并css、js,雪碧图/base64图片)
  2. 压缩(css、js、图片皆可压缩)
  3. 样式表放头部,脚本放底部
  4. 使用CDN(这部分,不少前端都不用考虑,负责发布的兄弟可能会负责搞好)
  5. 缓存……

等等……

有兴趣的同学,可以自行搜索雅虎关于前端优化的十四条规则。但这些规则当中,有多少是需要前端工程师付诸实践的?就我来说,CDN、缓存的设置,就是不需要我去关心的(作为一个苦逼外包,是没有权限去生产环境操作的)也就是说,一些优化的方法,可能只是需要前端工程师知道,说得难听点,就是应付面试,工作中遇不到。反而是一些工作中真正遇到的难题,面试中却很少被问到(纯属个人经验)。有些问题,同行们(包括我)可能稀里糊涂地用正确的方式解决了,但实际上,却并不知道自己为什么这样做。

前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM

以上三本书,有幸拜读。说不上醍醐灌顶,至少是让我认识到了差距。

我打算把自己的收获记录下来,希望对大家有所帮助。文中可能出现错误,希望大家能尽量用友善的语气提出来,谢谢!(づ ̄ 3 ̄)づ

不仅要避免去操作DOM,还要减少去访问DOM的次数。

在浏览器中,DOM和JS的实现,用的并不是同一个“东西”。比如说,我们最熟悉的chrome,JS引擎是V8,而DOM和渲染,靠的是WebCore库。也就是说,DOM和JS是两个独立的个体。

把DOM和JavaScript各自想象成一个岛屿,它们之间用收费桥梁连接。
                                                        --《高性能JavaScript》

1.添加页面元素,innerHTML vs DOM方法。

document.getElementById('test').innerHTML='<div>test</div>';
var t=document.createElement('div');
t.appendChild(document.createTextNode('test'));
document.getElementById('test').appendChild(t);

以上分别使用两种方法,向id='test'的元素中添加一个div。之前,大家可能一直被灌输的思想是innerHTML更快一些,真的是这样么?

还真是,至少在IE中是这样,但在基于webkit的新版浏览器中,使用DOM方法会稍快一些。所以,到底使用哪一种方法,还是应该有点争议的。我个人是喜欢innerHTML,因为用起来更简单。

此外,当需要添加大量相同的元素时,cloneNode比直接创建元素,稍微快一点。

2.访问元素的正确方法。

2.1 遍历集合vs遍历数组

当我们使用document.getElementsByName、document.getElementsByTagName、document.getElementsByClassName、docuemnt.images等方式来获取DOM元素时,我们得到的是一个HTML集合,这个集合始终与底层文档保持连接,每次去获取集合的信息时,都会重复执行一次查询。

var divs=document.getElementByTagName('div');
for(var i=0;i<divs.length;i++){
  document.body.append(document.createElement('div'))
}

如果不去运行,我们可能以为上面的代码会新添加几个div元素在页面中,但实际上,因为每次添加完一个div后,divs.length都会被更新(加一),所以,这个循环永远不会停止。解决办法非常简单

var divs=document.getElementByTagName('div');
for(var i=0,len=divs.length;i<len;i++){
  document.body.append(document.createElement('div'))
}

另外,HTML集合并不是一个数组,如果我们需要对这个集合进行遍历,可以先把它拷贝进一个数组,这样再遍历的时候,效率更高。

function toArray(coll){
  for(var i=0,a=[],len=coll.length;i<len;i++){
      a[i]=coll[i]
   }
  return a;
}

2.2访问元素属性

当遍历一个集合时,length属性应被缓存在循环外部,能够避免2.1中的逻辑错误;集合存储在局部变量中,也能够提高效率。此外,当对同一个DOM元素的属性进行访问时,把这个DOM缓存成一个局部变量,是更好的选择。

前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM
//只是做演示,真实情况中,当然没有这样的需求
//最差的方式
function fo1(){
var name='';
  for(var i=0;i<document.getElementsByTagName('div');i++){
    name=document.getElementsByTagName('div').nodeName;
    name=document.getElementsByTagName('div').nodeType;
   }
  return name;
}
//好一点的方式
function fo2(){
var name='';
var coll=document.getElementsByTagName('div');
for(var i=0,len=coll.length;i<len;i++){
  name=coll[i].nodeName;
  name=coll[i].nodeType;
}
return name;
}
//更好的方式
function fo3(){
  var name='';
  var coll=document.getElementsByTagName('div');
  var ele=null;
  for(var i=0,len=coll.length;i<len;i++){
    el=coll[i];
     name=el.nodeName;
     name=el.nodeType;
   }
}
前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM

3.选择器

前面已经提到,document.getElementsByName、document.getElementsByTagName、document.getElementsByClassName、docuemnt.images等方式,获取到的是HTML集合,效率低下;而querySelector以及querySelectorAll与之相比,得到的是一个NodeList,它是一个类数组对象,不会带来HTML集合的问题。而且,这个API在获取元素时,更加方便。唯一的问题,是要考虑目标浏览器是否提供支持。

4.重绘和重排

4.1何时重绘、重排?

重绘并不一定导致重排,比如修改某个元素的颜色,只会导致重绘;而重排之后,浏览器需要重新绘制受重排影响的部分。导致重排的原因有:

  • 添加或删除DOM元素
  • 元素位置、大小、内容改变
  • 浏览器窗口大小改变
  • 滚动条出现

因为重排和重绘的操作十分昂贵,浏览器会通过队列化修改并批量执行的方式,来进行优化(我的理解是,浏览器通过队列化和批量执行的方式,减少了重绘的次数)。比如:

//这段代码,并不会去重绘三次
var bodyStyle=document.body.style;
bodyStyle.color='red';
bodyStyle.color='black';
bodyStyle.color='green';

获取布局的操作,会导致队列刷新,浏览器的优化效果也就没有了。要避免在布局信息改变时,获取下列属性:

  • offsetTop,offsetLeft,offsetWidth,offsetHeight;
  • scrollTop,scrollLeft,scrollWidth,scrollHeight;
  • clientTop,clientLeft,clientWidth,clientHeight;
  • getComputedStyle()/currentStyle

4.2 最小化重排、重绘的建议

建议:不要再修改布局信息的时候,去查询布局信息

前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM
var computed;
var tmp='';
var bodyStyle=document.body.style;
if(document.body.currentStyle){
  computed=document.body.currentStyle
}else{
  computed=document.defaultView.getComputedStyle(document.body,'')
}
//bad
bodyStyle.color='red';
tmp=computed.backgroundColor;
bodyStyle.color='green';
tmp=computed.backgroundImage;
//good
bodyStyle.color='red';
bodyStyle.color='green';
tmp=computed.backgroundColor;
tmp=computed.backgroundImage;
前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM

修改一个元素的多个style时,一次性修改,而不是多次(虽然多次修改,经过现代浏览器的优化,也只会导致一次重排,但在老旧的浏览器中,仍然会导致多次)。建议:能用css的class解决的,就尽量不用内联样式。

:hover会降低响应速度,在处理很大的列表时,避免使用。

5事件委托

每绑定一个事件处理器,都是有代价的。如果有大量的元素需要绑定时间,尝试使用事件委托。分三步

  • 判断事件来源
  • 根据不同来源,进行不同操作
  • 取消冒泡,阻止默认行为(可选)
前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM
 document.querySelector('#nav').onclick=function (e) {
            if (e.target.nodeName=='A'){
                foo();
            }else{
                foo2()
            }
}
前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM

总结:

  • 减少DOM访问次数
  • 多次访问同一DOM,应该用局部变量缓存该DOM
  • 尽可能使用querySelector,而不是使用获取HTML集合的API
  • 注意重排和重绘
  • 使用事件委托,减少绑定事件的数量
  • 更多内容,可以阅读《高性能JavaScript》

写在前面:

大家都知道DOM的操作很昂贵。 

然后贵在什么地方呢?

一、访问DOM元素

二、修改DOM引起的重绘重排

一、访问DOM  

像书上的比喻:把DOM和JavaScript(这里指ECMScript)各自想象为一个岛屿,它们之间用收费桥梁连接,ECMAScript每次访问DOM,都要途径这座桥,并交纳“过桥费”,访问DOM的次数越多,费用也就越高。因此,推荐的做法是尽量减少过桥的次数,努力待在ECMAScript岛上。我们不可能不用DOM的接口,那么,怎样才能提高程序的效率?

既然无法避免,那就减少访问。(width、offsetTop、left。。。能少就少,可以缓存起来的,就缓存)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// code1错误
console.time(1);
for(var i = 0; i < times; i++) {
 document.getElementById('div1').innerHTML += 'a';
}
console.timeEnd(1);
  
// code2正确
console.time(2);
var str = '';
for(var i = 0; i < times; i++) {
 str += 'a';
}
document.getElementById('div2').innerHTML = str;
console.timeEnd(2);
////////////////////////

前端性能优化--为什么DOM操作慢?   浅谈DOM的操作以及性能优化问题-重绘重排  为什么要减少DOM操作  为什么要减少操作DOM

html集合&遍历DOM

html集合类似数组,但是跟数组还是不一样的。如: document.getElementsByTagName('a') 返回的html集合。这个集合是实时更新的,即后面代码修改了DOM,会反映在这个html集合里面。可尝试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
 <ul id='fruit'>
 <li> apple </li>
 <li> orange </li>
 <li> banana </li>
 </ul>
</body>
<script type="text/javascript">
 var lis = document.getElementsByTagName('li');
 var peach = document.createElement('li');
 peach.innerHTML = 'peach';
 document.getElementById('fruit').appendChild(peach);
  
 console.log(lis.length); // 4
</script>

正因为这个原因:html集合,读取 length 属性比数组消耗大多了。

要解决这个问题并不难,在遍历DOM集合的时候,缓存length就好了。不要每次使用就获取,主要体现在for循环中(你应该知道,for循环中,每一次都会执行判读语句,读取length)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.time(0);
var lis0 = document.getElementsByTagName('li');
var str0 = '';
for(var i = 0; i < lis0.length; i++) {
 str0 += lis0[i].innerHTML;
}
console.timeEnd(0);
  
console.time(1);
var lis1 = document.getElementsByTagName('li');
var str1 = '';
for(var i = 0, len = lis1.length; i < len; i++) {
 str1 += lis1[i].innerHTML;
}
console.timeEnd(1);

二、重绘重排

1.什么是重绘重排?

浏览器下载完页面中的所有组件——HTML标记、JavaScript、CSS、图片之后会解析生成两个内部数据结构——DOM树和渲染树。

在文档初次加载时,浏览器引擎通过解析 html文档 构建一棵DOM树,之后根据DOM元素的几何属性构建一棵用于展示渲染的渲染树。渲染树中的节点被称为“帧”或“盒",符合CSS模型的定义,可理解为(包括理解页面元素为一个具有大小,填充,边距,边框和位置的盒子)。由于隐藏元素不需要显示,渲染树中并不包含DOM树中隐藏的元素(知道这点有用)。 当渲染树构建完成,浏览器把每一个元素放到正确的位置上,然后再根据每一个元素的其他样式,绘制页面。

由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用table做布局的一个原因。

重绘:是一个元素外观的改变所触发的浏览器行为,例如改变visibility、outline、背景色等属性(上面说到的其他属性)。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,并不一定伴随重排。

重排:当DOM的变化影响了元素的几何属性(宽或高),浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排。重排一定伴随着重绘。

2. 触发重排的操作:

2.1 修改DOM元素几何属性:

修改元素大小,位置,内容(一般只有重绘,但是内容可能导致元素大小变化)

2.2 DOM树结构发生变化

当DOM树的结构变化时,例如节点的增减、移动等,也会触发重排。浏览器引擎布局的过程,类似于树的前序遍历,是一个从上到下从左到右的过程。 通常在这个过程中,当前元素不会再影响其前面已经遍历过的元素。所以,如果在body最前面插入一个元素,会导致整个文档的重新渲染,而在其后插入一个元 素,则不会影响到前面的元素。

2.4 改变浏览器大小

3.渲染树变化的排队和刷新

思考下面代码:

1
2
3
4
5
var ele = document.getElementById('myDiv');
ele.style.borderLeft = '1px';
ele.style.borderRight = '2px';
// var _top = ele.offsetTop; //刷新队列
ele.style.padding = '5px';

三行代码,三次修改元素的几何属性,浏览器应该发生三次重排重绘。

但是浏览器并不会这么笨,它也是有做优化的。它会把三次修改“保存”起来(大多数浏览器通过队列化修改并批量执行来优化重排过程,也有设置时间片段的),一次完成!

然而,如果你在三行代码中,以下获取DOM布局信息。(为了返回最新的布局信息,将立即执行渲染树变化队列的更新)

如上面被注释的第4行,如果取消注释会导致(2+3)、(5)两次重排;

获取关于DOM布局信息的属性:

1
2
3
4
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)

4 应对方法:尽量减少重绘次数、减少重排次数、缩小重排的影响范围。

4.1 合并多次操作,如上面的操作

ele.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

4.2 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

4.3 由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。但是这可能导致浏览器的闪烁。

4.4 在内存中多次操作节点,完成后再添加到文档中去(可使用fragment元素)。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的html片段,再一次性添加到文档中去,而不是循环添加每一行。

1
2
3
4
5
6
7
8
9
10
11
var fragment = document.createDocumentFragment();  // 未使用的虚拟节点,appendChild(fragment) //append的是里面的子元素
  
var li = document.createElement('li');
li.innerHTML = 'apple';
fragment.appendChild(li);
  
var li = document.createElement('li');
li.innerHTML = 'watermelon';
fragment.appendChild(li);
  
document.getElementById('fruit').appendChild(fragment);

以上就是小编为大家带来的浅谈DOM的操作以及性能优化问题-重绘重排全部内容了,希望大家多多支持脚本之家~

今天,在这学习vue,突然想到一个问题,我jQuery用的这么爽,看到什么元素,想操作dom就直接选中进行变化,好像没什么不方便的啊?那为啥我用的jQuery操作dom的构建页面的方法就这么被淘汰了呢?

那就问百度吧。

结果基本一面倒的都在说需要减少dom层的操作,原因,就是因为操作dom慢。

至于为啥慢,就涉及到浏览器渲染页面。浏览器分为:渲染引擎和js引擎。

渲染引擎工作:

  • 解析HTML代码,生产DOM tree
  • 解析CSS样式,结合DOM tree生产Render tree(display: none;的结点不会存在Render tree上,最后不会被paint)
  • 计算Render tree各个节点的布局信息,比如box的位置、尺寸、颜色、外形等
  • 根据计算后的布局信息,调用浏览器的UI引擎进行渲染。

而操作dom会产生几种动作,极大的影响渲染的效率。其中 layout(布局)和paint(绘制)是最大的。

  • layout 就是布局变动造成重新计算(耗CPU,有时也很耗内存)
  • paint 就是调用浏览器UI引擎进行渲染展示页面(耗CPU和内存)

其中大部分都应该算是摘抄自这个博客里的,讲的很透(https://my.oschina.net/u/1580821/blog/744684)

然后呢,在百度的过程中,又get到一个新鲜概念,虚拟DOM。

至于虚拟DOM呢,就直接把在百度上搜到的一个比较好的解释(其实算是与DOM的异同比较,但是感觉理解更容易)

虚拟DOM与真实DOM的区别(注意:需不需要虚拟DOM,其实与框架的DOM操作机制有关):

  1. 虚拟DOM不会进行排版与重绘操作

  2. 虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分(注意!),最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗

  3. 真实DOM频繁排版与重绘的效率是相当低的

  4. 虚拟DOM有效降低大面积(真实DOM节点)的重绘与排版,因为最终与真实DOM比较差异,可以只渲染局部(同2)

使用虚拟DOM的损耗计算:

总损耗 = 虚拟DOM增删改 + (与Diff算法效率有关)真实DOM差异增删改 + (较少的节点)排版与重绘

直接使用真实DOM的损耗计算:

总损耗 = 真实DOM完全增删改 + (可能较多的节点)排版与重绘

总之,一切为了减弱频繁的大面积重绘引发的性能问题,不同框架不一定需要虚拟DOM,关键看框架是否频繁会引发大面积的DOM操作

总之呢,因为这个没法绕开的问题,DOM操作就这么被主流的前端框架 所抛弃了。顺便也让我后知后觉的被主流拍到了沙滩上。

anyway,学吧,补充自己吧,加油!

为什么要减少操作DOM

2017年08月17日 12:44:29
阅读数:455

为什么要减少操作DOM? 
下面将按照以下布局同时操作div1和div2;然后在控制台打印出所需的时间,对时间尽心对比; 
以下是布局:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="div1"></div>
<div id="div2"></div>
<script>
   //这里写代码
</script>
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

对div1的操作如下:

console.time("interval1");
        var odiv = document.getElementById("div1");
        for (var i = 0, str = ""; i < 5000; i++) {
            odiv.innerHTML += "a"
        }
        console.timeEnd("interva11");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里在div1上连续操作了5000次,计时器interval1; 
对div2的操作如下:

console.time("interval2");
        var odiv2 = document.getElementById("div2");
        for (var i = 0, str = ""; i < 5000; i++) {
            str += "a"
        }
        odiv2.innerHTML = str;
        console.timeEnd("interval2");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里只操作了一次,同样完成了交互效果,计时器interval2; 
那么分别用了多久呢?

interva1: 1502944900429.3262ms
interval2: 0.5830078125ms
  • 1
  • 2

这就是documentCreateFragment的好处了 
也是为什么很多性能优化的书籍中说要减少dom操作额 
以下是完整代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="div1"></div>
<div id="div2"></div>
<script>
    window.onload = function () {
        console.time("interval1");
        var odiv = document.getElementById("div1");
        for (var i = 0, str = ""; i < 5000; i++) {
            odiv.innerHTML += "a"
        }
        console.timeEnd("interva1");
        console.time("interval2");
        var odiv2 = document.getElementById("div2");
        for (var i = 0, str = ""; i < 5000; i++) {
            str += "a"
        }
        odiv2.innerHTML = str;
        console.timeEnd("interval2");
    }
</script>
</body>
</html>