前端性能优化--为什么DOM操作慢? 浅谈DOM的操作以及性能优化问题-重绘重排 为什么要减少DOM操作 为什么要减少操作DOM
前端性能优化--为什么DOM操作慢?
作为一个前端,不能不考虑性能问题。对于大多数前端来说,性能优化的方法可能包括以下这些:
- 减少HTTP请求(合并css、js,雪碧图/base64图片)
- 压缩(css、js、图片皆可压缩)
- 样式表放头部,脚本放底部
- 使用CDN(这部分,不少前端都不用考虑,负责发布的兄弟可能会负责搞好)
- 缓存……
等等……
有兴趣的同学,可以自行搜索雅虎关于前端优化的十四条规则。但这些规则当中,有多少是需要前端工程师付诸实践的?就我来说,CDN、缓存的设置,就是不需要我去关心的(作为一个苦逼外包,是没有权限去生产环境操作的)也就是说,一些优化的方法,可能只是需要前端工程师知道,说得难听点,就是应付面试,工作中遇不到。反而是一些工作中真正遇到的难题,面试中却很少被问到(纯属个人经验)。有些问题,同行们(包括我)可能稀里糊涂地用正确的方式解决了,但实际上,却并不知道自己为什么这样做。
以上三本书,有幸拜读。说不上醍醐灌顶,至少是让我认识到了差距。
我打算把自己的收获记录下来,希望对大家有所帮助。文中可能出现错误,希望大家能尽量用友善的语气提出来,谢谢!(づ ̄ 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缓存成一个局部变量,是更好的选择。
//只是做演示,真实情况中,当然没有这样的需求 //最差的方式 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; } }
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 最小化重排、重绘的建议
建议:不要再修改布局信息的时候,去查询布局信息
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;
修改一个元素的多个style时,一次性修改,而不是多次(虽然多次修改,经过现代浏览器的优化,也只会导致一次重排,但在老旧的浏览器中,仍然会导致多次)。建议:能用css的class解决的,就尽量不用内联样式。
:hover会降低响应速度,在处理很大的列表时,避免使用。
5事件委托
每绑定一个事件处理器,都是有代价的。如果有大量的元素需要绑定时间,尝试使用事件委托。分三步
- 判断事件来源
- 根据不同来源,进行不同操作
- 取消冒泡,阻止默认行为(可选)
document.querySelector('#nav').onclick=function (e) { if (e.target.nodeName=='A'){ foo(); }else{ foo2() } }
总结:
- 减少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); //////////////////////// |
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操作机制有关):
虚拟DOM不会进行排版与重绘操作
虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分(注意!),最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗
真实DOM频繁排版与重绘的效率是相当低的
虚拟DOM有效降低大面积(真实DOM节点)的重绘与排版,因为最终与真实DOM比较差异,可以只渲染局部(同2)
使用虚拟DOM的损耗计算:
总损耗 = 虚拟DOM增删改 + (与Diff算法效率有关)真实DOM差异增删改 + (较少的节点)排版与重绘
直接使用真实DOM的损耗计算:
总损耗 = 真实DOM完全增删改 + (可能较多的节点)排版与重绘
总之,一切为了减弱频繁的大面积重绘引发的性能问题,不同框架不一定需要虚拟DOM,关键看框架是否频繁会引发大面积的DOM操作
总之呢,因为这个没法绕开的问题,DOM操作就这么被主流的前端框架 所抛弃了。顺便也让我后知后觉的被主流拍到了沙滩上。
anyway,学吧,补充自己吧,加油!
为什么要减少操作DOM
为什么要减少操作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>
前端性能优化--为什么DOM操作慢? 浅谈DOM的操作以及性能优化问题-重绘重排 为什么要减少DOM操作 为什么要减少操作DOM的更多相关文章
- 关于DOM的操作以及性能优化问题-重绘重排
写在前面: 大家都知道DOM的操作很昂贵. 然后贵在什么地方呢? 一.访问DOM元素 二.修改DOM引起的重绘重排 一.访问DOM 像书上的比喻:把DOM和JavaScript(这里指ECMScri ...
- DOM 操作成本究竟有多高,HTML、CSS构建过程 ,从什么方向出发避免重绘重排)
前言: 2019年!我准备好了 正文:从我接触前端到现在,一直听到的一句话:操作DOM的成本很高,不要轻易去操作DOM.尤其是React.vue等MV*框架的出现,数据驱动视图的模式越发深入人心,jQ ...
- 浅谈B+树索引的分裂优化(转)
http://www.tamabc.com/article/85038.html 从MySQL Bug#67718浅谈B+树索引的分裂优化 原文链接:http://hedengcheng.com/ ...
- 浅谈Spark应用程序的性能调优
浅谈Spark应用程序的性能调优 :http://geek.csdn.net/news/detail/51819 下面列出的这些API会导致Shuffle操作,是数据倾斜可能发生的关键点所在 1. g ...
- Dom 重绘重排
https://juejin.im/entry/590801780ce46300617c89b8 DOM 重绘重排
- 浅谈Unity中的GC以及优化
介绍: 在游戏运行的时候,数据主要存储在内存中,当游戏的数据不在需要的时候,存储当前数据的内存就可以被回收再次使用.内存垃圾是指当前废弃数据所占用的内存,垃圾回收(GC)是指将废弃的内存重新回收再次使 ...
- 浅谈Android样式开发之布局优化
引言 今天我们来谈一下Android中布局优化常用的一些手段.官方给出了3种优化方案,分别是</include>.</viewstub>.</merge>标签,下面 ...
- js 性能篇--dom 重绘 重排 节流
浏览器下载完页面中的所有组件----HTML标记,Js,CSS,图片等之后会解析并生成两个内部数据结构: DOM树 -------- 表示页面结构 渲染树 -------- 表示DOM节点如何显 ...
- 从MySQL Bug#67718浅谈B+树索引的分裂优化(转)
原文链接:http://hedengcheng.com/?p=525 问题背景 今天,看到Twitter的DBA团队发布了其最新的MySQL分支:Changes in Twitter MySQL 5. ...
随机推荐
- jQuery中的each关键词
#转载请留言联系 从一个简单的例子引入,jQuery里的each有什么作用? <!DOCTYPE html> <html lang="en"> <he ...
- vs2015 建立项目报错:值不能为空,参数名:path1的错误解决与“未将对象引用到对象的实例”
“值不能为空,参数名:path1” 的错误.原因就是安卓sdk的路径不正确. 最简单的解决办法如下: 找到C:\Program Files (x86)\Android\android-sdk.进入文件 ...
- Codeforces 1025D Recovering BST
这个题被wa成傻逼了.... ma[i][j]表示i,j能不能形成一条直接作为排序二叉树的边,n^3更新维护ma即可,按说应该是要爆复杂度的,数据玄学吧.. #include<iostream& ...
- codevs 1025 选菜——01背包
时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold 题解 查看运行结果 题目描述 Description 在小松宿舍楼下的不远处,有PK大学最不错的一个食堂—— ...
- mac-command-line-doing
创建文件夹 mkdir myDirectory 新建文件 touch a.html 编辑文件 vim a.html 删除文件 rm a.html 删除整个文件夹 rm -rf myDirectory ...
- HDOJ 3516 Tree Construction 四边形优化dp
原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=3516 题意: 大概就是给你个下凸包的左侧,然后让你用平行于坐标轴的线段构造一棵树,并且这棵树的总曼哈顿 ...
- JD
General Description: The role is responsible for China COE Operations Team’s data and cost analysis, ...
- hdu 1500 Chopsticks DP
题目链接:HDU - 1500 In China, people use a pair of chopsticks to get food on the table, but Mr. L is a b ...
- 每天一个linxu命令6之jps 查看java进程的端口
jps -- JavaVirtual Machine Process Status Tool 可以列出本机所有Java进程的pid jps [ options ] [ hostid ] 选项 -q 仅 ...
- UBIFS
转:http://www.armadeus.com/wiki/index.php?title=UBIFS This is a preliminary page dealing with the ins ...