JavaScript性能提升之——优化DOM操作

在web应用中,DOM操作一直属于是最常见的性能瓶颈,优化DOM操作就可以大幅度提升应用的速度,现今火热的React中所使用的虚拟DOM这一卖点也是为了尽量减少DOM操作而存在的优化方案,这一部分我们来具体说一说在DOM编程中的优化方案。

DOM与JavaScript

通常浏览器会将DOM与JavaScript独立实现,那么当我们访问DOM元素的时候实际上是一个独立的功能连接到另一个功能,而这个连接自然会产生性能消耗,每一次的访问都会产生消耗的话,尽可能的减少访问次数则成为优化的必然途径,那么具体有哪些方法呢。

减少DOM访问次数,尽量使用JavaScript处理

举一个简单的例子:
<pre>
//我希望在页面上输出 1-100
错误示范:
for(var i = 1;i≤100;i++){
document.getElementById('p1').innerHTML += ' ' + i;
}
</pre>
上面这段代码当然可以在页面上输出1-100的数字,但是注意,这里每次循环都会调用document.getElementById来访问DOM元素,那这里这个循环就访问了100次DOM。
简单修改一下:
<pre>
var strs = '';
for(var i = 1;i≤100;i++){
strs += ' ' + i;
}
document.getElementById('p1').innerHTML = strs;
</pre>
同样的效果,但是我们只访问了一次DOM,字符串的累积操作我们完全在JavaScript中做。

使用局部变量存储DOM引用

上例子:

//我希望点击一下按钮数字+1
function bindActions(){
    document.getElementById('button1').onclick=function(){
    document.getElementById('p1').innerHTML = parseInt(document.getElementById('p1').innerHTML) + 1;
   //jQuery 的$('#p1').html( parseInt($('#p1').html) + 1 ) 和上面是一样的 
  }
}
bindActions();

这个例子中,每次点击按钮都会让浏览器重新访问ID为P1的元素并让其原本数字+1,要多次访问的DOM元素,我们应该建立局部变量保存该元素的引用,以此减少DOM的访问。

//修改后
//我希望点击一下按钮数字+1
function bindActions(){
    //原生js
    var elm_button = document.getElementById('button1'),elm_textbox = document.getElementById('p1');
    elm_button .onclick=function(){
      elm_textbox .innerHTML = parseInt(elm_textbox .innerHTML) + 1;
    }

    //jQuery
    var $button = $('#button1'),$textbox = $('#p1');
    $button.on('click',function(){
       $textbox.html( parseInt( $textbox.html() ) + 1 );
    })
  }
  //js与jq的这两个命名方式是我的个人习惯
}
bindActions();

这个优化很简单实在,不过我觉得很多人还是嫌麻烦在用jquery的时候继续直接 $(selector)。。。还是要养成建立多次引用的局部变量好啊!!

为HTML集合做缓存
常见的HTML集合有

  • document.getElementsByTagName
  • document.getElementsByName
  • document.getElementsByClassName
  • document.links
  • document.images
  • document.forms
    这些都是返回HTML集合的属性,HTML集合是一个类数组对象,拥有与数组类似的length属性,也能使用数组下标来获取元素。
    使用HTML集合的时候,请尽量将其缓存,使用循环语句的时候也要将length缓存( 访问HTML集合的length属性比访问数组的length属性要慢很多 ),举个例子:
//bad
var elms_div = document.getElementsByTagName('div');
for(var i=0;i<elms_div.length;i++){
   elms_div[i].innerHTML= i ;
}
//good
var elms_div = document.getElementsByTagName('div');
for(var i=0,len=elms_div.length;i<len;i++){
   elms_div[i].innerHTML= i ;
}

另外一点要注意,HTML是具有实时性的,它会与文档一直保持着联系,意思即是你每次使用集合的时候,集合的数据都是最新的,用一段代码解释:

var divs = document.getElementsByTagName('div');
console.log(divs.length); // 3
document.body.appendChild(document.createElement('div'));
console.log(divs.length); //4

每次访问集合的时候他都会重新执行查询DOM的操作来返回最新的集合数据,这是需要注意的。

如果要使用的HTML集合的元素很多并且要频繁操作,可以将集合内的元素全部复制到数组中,数组的速度要比HTML集合快得多( 因为集合与文档时刻保持连接嘛 )

标准浏览器的原生DOM API
现代浏览器的原生JS中已经提供了一些速度更快的原生DOM方法,如querySelectorAll()
这个方法用起来很爽,类似使用css选择器:

var elms = document.querySelectorAll('#d1 p');

不仅方便,而且这个方法返回的不是HTML集合,而是一个类数组的对象NodeList,也正因为它不是HTMl集合,那么它自然就不会有集合的性能问题( 实时连接 )

重绘与重排

这一个点我觉得应该是大部分人都不会关注的,不说关注,就连知道什么是重绘和重排的人都挺少的我估计= =。

什么是重绘
要解释这一点,我觉得我应该先画个图

在DOM树中每个需要被显示的节点(display:none的元素不会在渲染树中)在渲染树中都会有对应的节点,CSS模型定义中,渲染树节点被称为" frames " 或 "boxes" ,上图基本就是浏览器在获取到资源后绘制页面的过程,可能会有点不同不过基本流程都差不多。

当DOM树与渲染树构建完成之后就浏览器就会绘制页面。

假如DOM产生了几何变化,那么与之对应的渲染树中的节点部分以及受到影响的部分都会失效,然后进行重新构建渲染树,这个就是重排的过程

重绘很好理解,在重排之后浏览器重新绘制被影响至失效的部分,这个过程就是重绘

重排与重绘都会对程序UI产生影响,尽量减少重排和重绘就是接下来我们要做的事。

什么时候会重排

布局与几何属性变动的时候浏览器就需要重排,对DOM元素的增加删除、改变位置、改变尺寸、改变内容、浏览器窗口尺寸修改都会产生重排,产生重排之后重绘是必然的,但是反过来,重绘的发生并不一定是因为重排。

只要不对页面布局以及几何属性修改就不需要重排,比如修改颜色只会发生重绘而并不需要重排(滚动条滚动会产生重排的哦)。

有兴趣的同学可以去用下Google的SpeedTracer来观测页面渲染过程

上面说了那么多的重绘和重排,其实就是为了说明在使用JS的时候改变DOM会有什么影响,如此看来,那么优化方案的中心点其实与前面说的很类似,减少DOM操作是关键

合并对DOM元素的修改操作可以优化,其次,减少重排还有下面几种:

  • 将元素先隐藏(display:none),更新完毕后再显示出来,举个例子:
常见的列表按条件排序、批量增加或删除纵列,先将其隐藏再排序结束后再放出,会比直接操纵列表省去更多的重排
  • 使用createDocumentFragment() 来更新节点。这个方法比较少人用,createDocumentFragment是一个document对象。它像是一个节点,我们可以朝里面添加子节点,然后使用appendChild(fragment)将其加入目标上,被添加进去的会是fragment的子节点,并且添加到目标对象的过程中只会触发一次重排且只访问一次实时DOM。

  • 为需要修改的节点做一个备份,然后操作副本,操作结束后替换旧的即可。简单概括就是先cloneNode,然后修改clone的,最后replaceChildj即可。

注意DOM动画

利用元素制作一些动画非常常见,这里要提到的是,在做动画的时候要使用absolute,脱离了文档流之后就算元素改变也只是小范围的重排重绘,否则处于文档流中变化的话,会产生大面积的多次重排重绘动作。

事件委托
使用事件委托来减少监听处理器的数量是非常有必要的,大量的事件绑定会让浏览器花费大量的资源来跟踪事件处理器。
父元素可以通过冒泡接收到其下所有元素的事件消息,通过这个特性,我们可以将多数的事件处理器绑定在父元素上,通过筛选是否需要触发的元素来触发事件。
举个例子:

//我想点击页面所有a标签弹出hello
//这里直接用jQ演示。。

//bad
$('a').on('click',function(){
  alert('hello')
});

//good
$(document.body).delegate('a','click',function(){
  alert('hello')
})

上述代码中两者都实现了点击a标签弹出hello的功能,但是代码二只监听了body就达到了这个效果,而代码一则给每个a标签都绑定了事件监听器,孰优孰劣不言而喻。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容