页面重绘与重排版的性能影响

DOM树和渲染树

  当浏览器下载完所有页面HTML 标记,JavaScript,CSS,图片之后,它解析文件并创建两个内部数据结构:一棵DOM树表示页面结构,一棵渲染树表示DOM节点如何显示。
      渲染树中为每个需要显示的DOM 树节点存放至少一个节点(隐藏DOM 元素在渲染树中没有对应节点)。渲染树上的节点称为“框”或者“盒”,符合CSS 模型的定义,将页面元素看作一个具有填充、边距、边框和位置的盒。一旦DOM 树和渲染树构造完毕,浏览器就可以显示(绘制)页面上的元素了。

重排版

       当DOM 改变影响到元素的几何属性(宽和高)——例如改变了边框宽度或在段落中添加文字,将发生一系列后续动作——浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变受到影响。浏览器使渲染树上受到影响的部分失效,然后重构渲染树。这个过程被称作重排版。重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受影响的部分。
      不是所有的DOM 改变都会影响几何属性。例如,改变一个元素的背景颜色不会影响它的宽度或高度。在这种情况下,只需要重绘(不需要重排版),因为元素的布局没有改变。
      重绘和重排版是负担很重的操作,可能导致网页应用的用户界面失去相应。所以,十分有必要尽可能减少这类事情的发生。

发生重排版情况

      正如前面所提到的,当布局和几何改变时需要重排版。在下述情况中会发生重排版:
      (1)添加或删除可见的DOM 元素
      (2)元素位置改变
      (3)元素尺寸改变(因为边距,填充,边框宽度,宽度,高度等属性改变)
      (4)内容改变,例如,文本改变或图片被另一个不同尺寸的所替代最初的页面渲染
      (5)浏览器窗口改变尺寸
      根据改变的性质,渲染树上或大或小的一部分需要重新计算。某些改变可导致重排版整个页面:例如,当一个滚动条出现时。因为计算量与每次重排版有关,大多数浏览器通过队列化修改和批量显示优化重排版过程。然而,你可能(经常不由自主地)强迫队列刷新并要求所有计划改变的部分立刻应用。获取布局信息的操作将导致刷新队列动作,这意味着使用了下面这些方法:
      offsetTop, offsetLeft, offsetWidth, offsetHeight
      scrollTop, scrollLeft, scrollWidth, scrollHeight
      clientTop, clientLeft, clientWidth, clientHeight
      getComputedStyle() (currentStyle in IE)(在IE 中此函数称为currentStyle)
      布局信息由这些属性和方法返回最新的数据,所以浏览器不得不运行渲染队列中待改变的项目并重新排版以返回正确的值。
      在改变风格的过程中,最好不要使用前面列出的那些属性。任何一个访问都将刷新渲染队列,即使你正在获取那些最近未发生改变的或者与最新的改变无关的布局信息。

重拍版实例分析

      1)考虑下面这个例子,它改变同一个风格属性三次(这也许不是你在真正的代码中所见到的,不过它孤立地展示出一个重要话题):
    

     // setting and retrieving styles in succession
      var computed,
      tmp = '',
      bodystyle = document.body.style;
      if (document.body.currentStyle) { // IE, Opera
            computed = document.body.currentStyle;
      } else { // W3C
            computed = document.defaultView.getComputedStyle(document.body, '');
      }
      // inefficient way of modifying the same property
      // and retrieving style information right after
      bodystyle.color = 'red';
      tmp = computed.backgroundColor;
      bodystyle.color = 'white';
      tmp = computed.backgroundImage;
      bodystyle.color = 'green';
      tmp = computed.backgroundAttachment;

      在这个例子中,body 元素的前景色被改变了三次,每次改变之后,都导入computed 的风格。导入的属性backgroundColor, backgroundImage, 和backgroundAttachment 与颜色改变无关。然而,浏览器需要刷新渲染队列并重排版,因为computed 的风格被查询而引发。

      比这个不讲效率的例子更好的方法是不要在布局信息改变时查询它。如果将查询computed 风格的代码搬到末尾,代码看起来将是这个样子:
     

      bodystyle.color = 'red';
      bodystyle.color = 'white';
      bodystyle.color = 'green';
      tmp = computed.backgroundColor;
      tmp = computed.backgroundImage;
      tmp = computed.backgroundAttachment; 

      在所有浏览器上,第二个例子将更快,重排版和重绘代价昂贵,所以,提高程序响应速度一个好策略是减少此类操作发生的机会。为减少发生次数,你应该将多个DOM 和风格改变合并到一个批次中一次性执行。

      2)考虑这个例子:
    

      var el = document.getElementById('mydiv');
      el.style.borderLeft = '1px';
      el.style.borderRight = '2px';
      el.style.padding = '5px';

      这里改变了三个风格属性,每次改变都影响到元素的几何属性。在这个糟糕的例子中,它导致浏览器重排版了三次。大多数现代浏览器优化了这种情况只进行一次重排版,但是在老式浏览器中,或者同时有一个分离的同步进程(例如使用了一个定时器),效率将十分低下。如果其他代码在这段代码运行时查询布局信息,将导致三次重布局发生。而且,此代码访问DOM 四次,可以被优化。

      一个达到同样效果而效率更高的方法是:将所有改变合并在一起执行,只修改DOM 一次。可通过使用
      cssText 属性实现:
      var el = document.getElementById('mydiv');
      el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';
      这个例子中的代码修改cssText 属性,覆盖已存在的风格信息。如果你打算保持当前的风格,你可以将它附加在cssText 字符串的后面。
      el.style.cssText += '; border-left: 1px;';
另一个一次性改变风格的办法是修改CSS 的类名称,而不是修改内联风格代码。这种方法适用于那些风格不依赖于运行逻辑,不需要计算的情况。改变CSS 类名称更清晰,更易于维护;它有助于保持脚本免除显示代码,虽然它可能带来轻微的性能冲击,因为改变类时需要检查级联表。
      var el = document.getElementById('mydiv');
      el.className = 'active';

批量修改DOM如何减少重排版次数

      当你需要对DOM 元素进行多次修改时,你可以通过以下步骤减少重绘和重排版的次数:
      1.从文档流中摘除该元素
      2.对其应用多重改变
      3.将元素带回文档中
      此过程引发两次重排版——第一步引发一次,第三步引发一次。如果你忽略了这两个步骤,那么第二步
中每次改变都将引发一次重排版。
      有三种基本方法可以将DOM 从文档中摘除:
      1) 隐藏元素,进行修改,然后再显示它。
      2) 使用一个文档片断在已存DOM 之外创建一个子树,然后将它拷贝到文档中。
      3) 将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。
      为演示脱离文档操作,考虑这样一个链接列表,它必须被更多的信息所更新:

 <ul id="mylist">
       <li><a href="http://phpied.com">Stoyan</a></li>
       <li><a href="http://julienlecomte.com">Julien</a></li>
 </ul>

      假设附加数据已经存储在一个对象中了,需要插入到这个列表中。这些数据定义如下:      

var data = [
            {
                  "name": "Nicholas",
                  "url": "http://nczonline.net"
            },
            {
                  "name": "Ross",
                  "url": "http://techfoolery.com"
            }
      ];

 

      下面是一个通用的函数,用于将新数据更新到指定节点中:
    

  function appendDataToElement(appendToElement, data) {
            var a, li;
            for (var i = 0, max = data.length; i < max; i++) {
                  a = document.createElement('a');
                  a.href = data[i].url;
                  a.appendChild(document.createTextNode(data[i].name));
                  li = document.createElement('li');
                  li.appendChild(a);
                  appendToElement.appendChild(li);
            }
      };

 

      将数据更新到列表而不管重排版问题,最明显的方法如下:
      var ul = document.getElementById('mylist');
      appendDataToElement(ul, data);
      使用这个方法,然而,data 队列上的每个新条目追加到DOM 树都会导致重排版。

  如前面所讨论过的,减少重排版的一个方法是通过改变display 属性,临时从文档上移除<ul>元素然后再恢复它。      

      var ul = document.getElementById('mylist');
      ul.style.display = 'none';
      appendDataToElement(ul, data);
      ul.style.display = 'block';

      另一种减少重排版次数的方法是:在文档之外创建并更新一个文档片断,然后将它附加在原始列表上。文档片断是一个轻量级的document 对象,它被设计专用于更新、移动节点之类的任务。文档片断一个便利的语法特性是当你向节点附加一个片断时,实际添加的是文档片断的子节点群,而不是片断自己。下面的例子减少一行代码,只引发一次重排版,只触发“存在DOM”一次。

      var fragment = document.createDocumentFragment();
      appendDataToElement(fragment, data);
      document.getElementById('mylist').appendChild(fragment);

      第三种解决方法首先创建要更新节点的副本,然后在副本上操作,最后用新节点覆盖老节点:

      var old = document.getElementById('mylist');
      var clone = old.cloneNode(true);
      appendDataToElement(clone, data);
      old.parentNode.replaceChild(clone, old);

      推荐尽可能使用文档片断(第二种解决方案)因为它涉及最少数量的DOM 操作和重排版。唯一潜在的缺点是,当前文档片断还没有得到充分利用,开发者可能不熟悉此技术。

      浏览器通过队列化修改和批量运行的方法,尽量减少重排版次数。当你查询布局信息如偏移量、滚动条位置,或风格属性时,浏览器刷队列并执行所有修改操作,以返回最新的数值。最好是尽量减少对布局信息的查询次数,查询时将它赋给局部变量,并用局部变量参与计算。
      考虑一个例子,将元素myElement 向右下方向平移,每次一个像素,起始于100×100 位置,结束于500×500位置,在timeout 循环体中你可以使用:

      // inefficient
      myElement.style.left = 1 + myElement.offsetLeft + 'px';
      myElement.style.top = 1 + myElement.offsetTop + 'px';
      if (myElement.offsetLeft >= 500) {
            stopAnimation();
      }


      这样做很没效率,因为每次元素移动,代码查询偏移量,导致浏览器刷新渲染队列,并没有从优化中获益。另一个办法只需要获得起始位置值一次,将它存入局部变量中var current = myElement.offsetLeft;。然后,在动画循环中,使用current 变量而不再查询偏移量:

      current++
      myElement.style.left = current + 'px';
      myElement.style.top = current + 'px';
      if (current >= 500) {
            stopAnimation();
      }

转载自:http://exception.thinksaas.cn/0/16/16591.html

posted @ 2017-06-26 11:53  方帅  阅读(1791)  评论(0编辑  收藏  举报