转自:https://feclub.cn/post/content/domcss
经过js操做DOM的代价很高,影响页面性能的主要问题有以下几点:html
DOM的修改会致使重绘和重排。前端
页面重绘的速度要比页面重排的速度快,在页面交互中要尽可能避免页面的重排操做。浏览器不会在js执行的时候更新DOM,而是会把这些DOM操做存放在一个队列中,在js执行完以后按顺序一次性执行完毕,所以在js执行过程当中用户一直在被阻塞。vue
一个页面更新时,渲染过程大体以下:jquery
在网页生成的时候,至少会进行一次布局和渲染,在后面用户的操做时,不断的进行重绘或重排,所以若是在js中存在不少DOM操做,就会不断地出发重绘或重排,影响页面性能。git
如前面所说,DOM操做影响页面性能的核心问题主要在于DOM操做致使了页面的重绘或重排,为了减小因为重绘和重排对网页性能的影响,咱们要知道都有哪些操做会致使页面的重绘或者重排。github
接下来会分享一下在平时项目中因为高频操做DOM影响网页性能的问题。web
在最近作的抽奖项目中,就遇到了这样的因为高频操做DOM,致使页面性能变差的问题。在经历几轮抽奖后,文字滚动速度愈来愈慢,肉眼能感觉到与第一次抽奖时文字滚动速度的明显差异,如持续时间过长或轮次过多,还会形成浏览器假死现象。数组
实现demo: https://gxt19940130.github.io/demo/dom.html浏览器
下图为抽奖时文字滚动过程当中的timeline记录。
timeline分析:
- FPS:最上面一栏为绿色柱形为帧率(FPS),顶点值为60fps,上方红色方块表示长帧,这些长帧被Chrome称为jank(卡顿)。
- CPU:第二栏为CPU,蓝色表示loading(网络通讯和HTML解析),黄色表示scripting(js执行时间),紫色表示rendering(样式计算和布局,即重排), 绿色为painting(即重绘)。
更多timeline使用方法可参考:如何使用Chrome Timeline 工具(译)
由上图能够看出,在文字滚动过程当中红色方块出现频繁,页面中存在的卡顿过多。帧率的值越低,人眼感觉到的效果越差。
参考文章:脑洞大开:为啥帧率达到 60 fps 就流畅?
接下来选择一段长帧区域放大来看
在这段区域内最大一帧达到了49.7ms,帧率只有20fps,接下来看看这一帧里是什么因素耗时过长
由上图能够看出,耗时最大的在scripting,js的执行时间达到了44.9ms,占总时间的93.2%,由于主要靠js计算控制DOM的显示内容,因此js运行时间过长。
选取一段FPS值很低的部分查看形成这段值低的缘由
由下图可看出主要为dom.html中的js执行占用时间。
点进dom.html文件,便可定位到该函数
由此可知,主要是rolling这个函数执行时间过长,对该部分失帧影响较大。而这个函数的主要做用就是实现文字的滚动效果,也能够从代码中看出,这个函数利用的setTimeout来反复执行,而且在这个函数中存在着循环以及大量的DOM操做,形成了页面的失帧等问题。
针对该项目中的问题,采起的解决方法是:
一次性生成所有
requestAnimationFrame与setTimeout和setInterval相似,都是经过递归调用同一个方法不断更新页面。
- setTimeout():在特定的时间后执行函数,并且只执行一次,若是在特定时间前想取消执行函数,能够用clearTimeout当即取消执行。可是并非每次执行setTimeout都会在特定的时间后执行,页面加载后js会按照主线程中的顺序按序执行那个,若是在延迟时间内主线程不空闲,setTimeout里面的函数是不会执行的,它会延迟到主线程空闲时才执行。
- setInterval():在特定的时间间隔内重复执行函数,除非主动清除它,否则会一直执行下去,清除函数可使用clearInterval。setInterval也会等到主线程空闲了再执行,可是setInterval去排队时,若是发现本身还在队列中未执行,就会被drop掉,因此可能会形成某段时间的函数未被执行。
- requestAnimationFrame():它不须要设置时间间隔,它会在浏览器每次刷新以前执行回调函数的任务。这样咱们动画的更新就能和浏览器的刷新频率保持一致。requestAnimationFrame在运行时,浏览器会自动优化方法的调用,而且若是页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。
在采用上面的方法进行优化后,在经历多轮抽奖后,文字滚动速度依旧正常,网页性能良好,不会出现文字滚动速度愈来愈慢,最后致使浏览器假死的现象。
实现demo: https://gxt19940130.github.io/demo/demo_gxt/dom_by_vue.html
优化前文字滚动时的timeline
优化后文字滚动时的timeline
优化前的代码对DOM操做很频繁,所以FPS值广泛偏低,而优化后能够看出红色方块明显减小,FPS值一直处于高值。
优化前文字滚动时的timeline
优化后文字滚动时的timeline
优化前js的CPU占用率较高,而优化后占用CPU的主要为渲染时间,由于优化后的代码只是控制了节点的显示和隐藏,因此在js上消耗较少,在渲染上消耗较大。
吸顶导航条要求当页面滚动到某个区域时,对应该区域的导航条在设置的显示范围内保持吸顶显示。涉及到的操做:
因为scroll事件被触发的频率高、间隔近,若是此时进行DOM操做或计算而且这些DOM操做和计算没法在下一次scroll事件发生前完成,就会形成掉帧、页面卡顿,影响用户体验。
针对该项目中的问题,采起的解决方法是:
// 在页面滚动时对显示范围进行计算 // 延迟到整个dom加载完后再调用,而且异步到全部事件后执行 $(function(){ //animationShow优化滚动效果,scrollShow为实际计算显示范围及操做DOM的函数 setTimeout( function() { window.Scroller.on('scrollend', animationShow); window.Scroller.on('scrollmove', animationShow); }) }); function animationShow(){ return window.requestAnimationFrame ?window.requestAnimationFrame(scrollShow) : scrollShow(); }
对于scroll的滚动优化还能够采用防抖(Debouncing)和节流(Throttling)的方式,可是防抖和节流的方式仍是要借助于setTimeout,所以和requestAnimationFrame相比,仍是requestAnimationFrame实现效果好一些。
参考文章:高性能滚动 scroll 及页面渲染优化
为了减小DOM操做对页面性能产生的影响,在实现页面的交互效果时必定要注意一下几点:
//优化前代码 function Loop() { console.time("loop1"); for (var count = 0; count < 15000; count++) { document.getElementById('text').innerHTML += 'dom'; } console.timeEnd("loop1"); }
//优化后代码 function Loop2() { console.time("loop2"); var content = ''; for (var count = 0; count < 15000; count++) { content += 'dom'; } document.getElementById('text2').innerHTML += content; console.timeEnd("loop2"); }
两个函数的执行时间对比:
优化前的代码中,每进行一次循环,都会读取一次div的innerHtml属性,而且对这个属性进行了从新赋值,即每循环一次就会操做两次DOM,所以执行时间很长,页面性能差。
在优化后的代码中,将要更新的DOM内容进行缓存,在循环时只操做字符串,循环结束后字符串的值写入到div中,只进行了一次查找innerHtml属性和一次对该属性从新赋值的操做,所以一样的循环次数先,优化后的方法执行时间远远少于优化前。
在抽奖项目中频繁操做DOM来控制文字滚动的方法(demo:https://gxt19940130.github.io/demo/dom.html 致使页面性能不好,最后修改成以下代码。
<div class="staff-list" :class="list"> <ul class="staff-list-ul"> <li v-for="item in staffList" v-show="isShow($index)"> <div>{{{item.staff_name | addSpace}}} </div> <div class="staff_phone">{{item.phone_no}} </div> </li> </ul> </div>
上面代码的优化原理即先生成全部DOM节点,可是全部节点均不显示出来,利用vue.js中的v-show,根据计算的随机数来控制显示某个
若是采用jquery,则须要将生成的全部
对比结果可查看2.4
var list1 = $(".list1"); list1.hide(); for (var i = 0; i < 15000; i++) { var item = document.createElement("li"); item.append(document.createTextNode('0')); list1.append(item); } list1.show();
display属性值为none的元素不在渲染树中,所以对隐藏的元素操做不会引起其余元素的重排。若是要对一个元素进行屡次DOM操做,能够先将其隐藏,操做完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操做时都出发一次重排。
页面rendering时间对比:
下图为一样的循环次数下未隐藏节点直接进行DOM操做的rendering时间(图一)和隐藏节点再进行DOM操做的rendering时间(图二)
由对比图能够看出,总时间、js执行时间以及rendering时间都明显减小,而且避免了painting以及其余的一些操做。
//优化前代码 var element = document.getElementById('mydiv'); element.style.height = "100px"; element.style.borderLeft = "1px"; element.style.padding = "20px";
在上面的代码中,每对element进行一次样式更改都会影响该元素的集合结构,最糟糕状况下会触发三次重排。
优化方式:利用js或jquery对该元素的class从新赋值,得到新的样式,这样减小了屡次的DOM操做。
//优化后代码 //js操做 .newStyle { height: 100px; border-left: 1px; padding: 20px; } element.className = "newStyle"; //jquery操做 $(element).css({ height: 100px; border-left: 1px; padding: 20px; })
到此本文结束,若是对于问题分析存在不正确的地方,还请及时指出,多多交流。
参考文章: