DOM的修改会导致 重绘
和 重构
,重绘意味着网页样式的改变比如背景颜色、字体颜色等,重构意味着结构的改变,消耗性能要大于重绘,浏览器不会在js执行的时候更新dom,而是会把这些dom操作存放在一个队列中,在js执行完之后按顺序一次性执行完毕,因此在js执行过程中用户一直在被阻塞。
在最近做的年会抽奖项目中,就遇到了这样的高频操作DOM,严重影响页面性能的问题,在经历几轮抽奖后,文字滚动速度越来越慢,肉眼能感受到与第一次抽奖时文字滚动速度的明显差别,如持续时间过长或轮次过多,还会造成浏览器假死现象。
实现demo: https://gxt19940130.github.io/demo/dom.html
衡量页面性能一个重要的指标是fps,即帧率(每秒帧数),帧率越高,页面运行越流畅。
由下图demo的timeline可以看出,fps显示为红色的占多数,这个demo中的帧率多数在20~45fps之间,页面会出现严重的掉帧的情况,当帧率低于24fps时,肉眼就会感觉到页面存在卡顿现象,所以用这种频繁操作DOM来实现文字滚动效果的方法写出的页面性能很差。
针对该项目中的问题,采取的解决方法是:
<li>
,并且隐藏这些 <li>
,随机生成一组随机数数组,只有index与数组里面的随机数相等时,才显示该位置的 <li>
。 requestAnimationFrame
取代 setTimeout
不断生成随机数。 requestAnimationFrame与setTimeout和setInterval类似,都是通过递归调用同一个方法不断更新页面。但是setTimeout和setInterval都存在性能上的问题,而requestAnimationFrame在运行时,浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。
在采用上面的方法进行优化后,在经历多轮抽奖后,文字滚动速度依旧正常,网页性能良好,不会出现文字滚动速度越来越慢,最后导致浏览器假死的现象。
顶部导航条要求当页面滚动到某个区域时,对应该区域的导航条在设置的显示范围内吸顶显示,因此需要监听页面的scroll事件,并在页面滚动时进行计算和DOM操作。
// 在页面滚动时对显示范围进行计算 // 延迟到整个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事件被触发的频率高、间隔近,如果此时进行DOM操作或计算并且这些DOM操作和计算无法在下一次scroll事件发生前完成,就会造成掉帧、页面卡顿,影响用户体验。
针对该项目中的问题,采取的解决方法是:
页面加载时,浏览器会根据HTML构建DOM树,再根据CSS和DOM树构建渲染树。如前面所说, DOM操作影响页面性能的核心问题主要是页面的重绘和重排
。
导致页面重排的一些操作:
offsetTop
、 offsetLeft
、 offsetWidth
、 offsetHeight
、 scrollTop
、 scrollLeft
、 scrollWidth
、 scrollHeight
、 clientTop
、 clientLeft
、 clientWidth
、 clientHeight
、 getComputedStyle()
。 导致页面重绘的操作
//优化前代码 var _li = $("<li>"), _dom = $("<div>"), timer = null; for (var i = 0; i < 50; i++) { //随机生成50个li,插入到ul列表中 $(".list-ul").append(_li.clone()); }
//优化后代码 var _li = $("<li>"), _dom = $("<div>"), _lis = document.getElementsByTagName("li"), timer = null, _arr = []; for (var i = 0; i < 50; i++) { //随机生成50个li,存入到数组中 _arr.push(_li.clone()); } //将生成好的全部li一次性append到ul中 $(".list-ul").append(_arr);
优化前的代码中,对于 $(".list-ul")
元素进行了50次的append,即进行了50次的DOM操作。而对于优化后的代码,在append操作前,先将所有 <li>
存入数组中,最后只进行了一次append,因此性能会有所提高。
在年会抽奖项目中频繁操作DOM来控制文字滚动的方法( demo ),导致页面性能很差,最后修改为如下代码。
<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
,根据计算的随机数来控制显示某个 <li>
,来达到文字滚动效果。
如果采用jquery,则需要将生成的所有 <li>
全部存放在 <ul>
下,并且隐藏它们,在根据生成的随机数组,利用jquery查找index与生成的随机数对应的 <li>
并显示,达到文字滚动效果。
list.style.display = "none"; for (var i=0; i < items.length; i++){ var item = document.createElement("li"); item.appendChild(document.createTextNode("Option " + i); list.appendChild(item); } list.style.display = "";
display属性值为none的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行多次DOM操作,可以先将其隐藏,操作完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操作时都出发一次重排。
//优化前代码 element.style.backgroundColor = "blue"; element.style.color = "red"; element.style.fontSize = "20px";
//优化后代码 //js操作 .newStyle { background-color: blue; color: red; font-size: 20px; } element.className = "newStyle"; //jquery操作 $(element).css({ background-color: blue; color: red; font-size: 20px; })
优化前的代码每一次更改样式都会查找一次该元素进行一次DOM操作,而优化后的代码,对于要修改的几个样式,都是只进行一次查找操作,因此只进行了一次DOM操作,避免了多次重绘或者重排。