高频dom操作和页面性能优化探索

CesHylton 8年前
   <h2>一、高频操作DOM会导致的问题</h2>    <p>DOM的修改会导致 重绘 和 重构 ,重绘意味着网页样式的改变比如背景颜色、字体颜色等,重构意味着结构的改变,消耗性能要大于重绘,浏览器不会在js执行的时候更新dom,而是会把这些dom操作存放在一个队列中,在js执行完之后按顺序一次性执行完毕,因此在js执行过程中用户一直在被阻塞。</p>    <h3>1.年会抽奖项目的高频操作DOM问题</h3>    <p>在最近做的年会抽奖项目中,就遇到了这样的高频操作DOM,严重影响页面性能的问题,在经历几轮抽奖后,文字滚动速度越来越慢,肉眼能感受到与第一次抽奖时文字滚动速度的明显差别,如持续时间过长或轮次过多,还会造成浏览器假死现象。</p>    <p>实现demo: <a href="/misc/goto?guid=4959735941155934279" rel="nofollow,noindex">https://gxt19940130.github.io/demo/dom.html</a></p>    <p>衡量页面性能一个重要的指标是fps,即帧率(每秒帧数),帧率越高,页面运行越流畅。</p>    <p>由下图demo的timeline可以看出,fps显示为红色的占多数,这个demo中的帧率多数在20~45fps之间,页面会出现严重的掉帧的情况,当帧率低于24fps时,肉眼就会感觉到页面存在卡顿现象,所以用这种频繁操作DOM来实现文字滚动效果的方法写出的页面性能很差。</p>    <p><img src="https://simg.open-open.com/show/efeb57b2d53bf06f4e4cfd6ec0b7e034.png"> <img src="https://simg.open-open.com/show/1aa4afc11fff82f5f5563caa812a8013.png"></p>    <p>针对该项目中的问题,采取的解决方法是:</p>    <ul>     <li>一次性生成全部 <li> ,并且隐藏这些 <li> ,随机生成一组随机数数组,只有index与数组里面的随机数相等时,才显示该位置的 <li> 。</li>     <li>用 requestAnimationFrame 取代 setTimeout 不断生成随机数。 <p>requestAnimationFrame与setTimeout和setInterval类似,都是通过递归调用同一个方法不断更新页面。但是setTimeout和setInterval都存在性能上的问题,而requestAnimationFrame在运行时,浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。</p> </li>    </ul>    <p>在采用上面的方法进行优化后,在经历多轮抽奖后,文字滚动速度依旧正常,网页性能良好,不会出现文字滚动速度越来越慢,最后导致浏览器假死的现象。</p>    <h3>2.顶部导航条相关及scroll滚动优化</h3>    <p>顶部导航条要求当页面滚动到某个区域时,对应该区域的导航条在设置的显示范围内吸顶显示,因此需要监听页面的scroll事件,并在页面滚动时进行计算和DOM操作。</p>    <pre>  <code class="language-javascript">// 在页面滚动时对显示范围进行计算  // 延迟到整个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();  }</code></pre>    <p>scroll事件被触发的频率高、间隔近,如果此时进行DOM操作或计算并且这些DOM操作和计算无法在下一次scroll事件发生前完成,就会造成掉帧、页面卡顿,影响用户体验。</p>    <p>针对该项目中的问题,采取的解决方法是:</p>    <ul>     <li>尽量控制DOM的显示或隐藏,而不是删除或添加。页面加载时根据当前页面中吸顶导航的数量复制对应的DOM,并且隐藏这些导航。当页面滚动到指定区域后,显示对应的导航。</li>     <li>一次性操作DOM,将复制的DOM存储到数组中,将该数组append到对应的父节点下,而不是根据复制得到DOM的数量依次循环插入到父节点下。</li>     <li>多做缓存,如果某个节点将在后续进行多次操作,可以将该节点利用变量存储起来,而不是每次进行操作时都去查找一遍该节点。</li>    </ul>    <h2>二、DOM操作影响页面性能的核心问题</h2>    <p>页面加载时,浏览器会根据HTML构建DOM树,再根据CSS和DOM树构建渲染树。如前面所说, DOM操作影响页面性能的核心问题主要是页面的重绘和重排 。</p>    <ul>     <li>重绘是指一些样式的修改,元素的位置和大小都没有改变;</li>     <li>重排是指元素的位置或尺寸发生了变化,浏览器需要重新计算渲染树,而新的渲染树建立后,浏览器会重新绘制受影响的元素。因此页面重绘的速度要比页面重排的速度快,在页面交互中要尽量避免页面的重排操作。</li>    </ul>    <p>导致页面重排的一些操作:</p>    <ul>     <li>DOM元素的几何属性的变化      <ul>       <li>例如改变DOM元素的宽高值时,原渲染树中的相关节点会失效,浏览器会根据变化后的DOM重新构建渲染树中的相关节点。如果父节点的几何属性变化时,还会使其子节点及后续兄弟节点重新计算位置等,造成一系列的重排。</li>      </ul> </li>     <li>DOM树的结构变化      <ul>       <li>添加DOM节点、修改DOM节点位置及删除某个节点都是对DOM树的更改,会造成页面的重排。浏览器布局是从上到下的过程,修改当前元素不会对其前边已经遍历过的元素造成影响,但是如果在所有的节点前添加一个新的元素,则后续的所有元素都要进行重排。</li>      </ul> </li>     <li>获取某些属性      <ul>       <li>除了渲染树的直接变化,当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括: offsetTop 、 offsetLeft 、 offsetWidth 、 offsetHeight 、 scrollTop 、 scrollLeft 、 scrollWidth 、 scrollHeight 、 clientTop 、 clientLeft 、 clientWidth 、 clientHeight 、 getComputedStyle() 。</li>      </ul> </li>     <li>浏览器窗口尺寸改变      <ul>       <li>窗口尺寸的改变会影响整个网页内元素的尺寸的改变,即DOM元素的集合属性变化,因此会造成重排。</li>      </ul> </li>    </ul>    <p>导致页面重绘的操作</p>    <ul>     <li>应用新的样式或者修改任何影响元素外观的属性      <ul>       <li>只改变了元素的样式,并未改变元素大小、位置,此时只涉及到重绘操作。</li>      </ul> </li>     <li>重排一定会导致重绘      <ul>       <li>一个元素的重排一定会影响到渲染树的变化,因此也一定会涉及到页面的重绘。</li>      </ul> </li>    </ul>    <h2>三、针对操作DOM的性能优化方法</h2>    <h3>1.减少在循环内进行DOM操作,在循环外部进行DOM缓存</h3>    <pre>  <code class="language-javascript">//优化前代码  var _li = $("<li>"),      _dom = $("<div>"),      timer = null;  for (var i = 0; i < 50; i++) {   //随机生成50个li,插入到ul列表中      $(".list-ul").append(_li.clone());  }</code></pre>    <pre>  <code class="language-javascript">//优化后代码  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);</code></pre>    <p>优化前的代码中,对于 $(".list-ul") 元素进行了50次的append,即进行了50次的DOM操作。而对于优化后的代码,在append操作前,先将所有 <li> 存入数组中,最后只进行了一次append,因此性能会有所提高。</p>    <h3>2.只控制DOM节点的显示或隐藏,而不是直接去改变DOM结构</h3>    <p>在年会抽奖项目中频繁操作DOM来控制文字滚动的方法( <a href="/misc/goto?guid=4959735941155934279" rel="nofollow,noindex">demo</a> ),导致页面性能很差,最后修改为如下代码。</p>    <pre>  <code class="language-javascript"><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></code></pre>    <p>上面代码的优化原理即先生成所有DOM节点,但是所有节点均不显示出来,利用vue.js中的 v-show ,根据计算的随机数来控制显示某个 <li> ,来达到文字滚动效果。</p>    <p>如果采用jquery,则需要将生成的所有 <li> 全部存放在 <ul> 下,并且隐藏它们,在根据生成的随机数组,利用jquery查找index与生成的随机数对应的 <li> 并显示,达到文字滚动效果。</p>    <h3>3.操作DOM前,先把DOM节点删除或隐藏</h3>    <pre>  <code class="language-javascript">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 = "";</code></pre>    <p>display属性值为none的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行多次DOM操作,可以先将其隐藏,操作完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操作时都出发一次重排。</p>    <h3>4.一次性修改样式和属性,不要每次只修改一个</h3>    <pre>  <code class="language-javascript">//优化前代码  element.style.backgroundColor = "blue";    element.style.color = "red";    element.style.fontSize = "20px";</code></pre>    <pre>  <code class="language-javascript">//优化后代码  //js操作  .newStyle {        background-color: blue;        color: red;        font-size: 20px;    }    element.className = "newStyle";  //jquery操作  $(element).css({      background-color: blue;        color: red;        font-size: 20px;   })</code></pre>    <p>优化前的代码每一次更改样式都会查找一次该元素进行一次DOM操作,而优化后的代码,对于要修改的几个样式,都是只进行一次查找操作,因此只进行了一次DOM操作,避免了多次重绘或者重排。</p>    <p> </p>    <p>来自:http://feclub.cn/post/content/dom</p>    <p> </p>