重绘重排重渲染

FROM ME:javascript

重绘重排重渲染是浏览器呈现页面时须要经历的过程,处理得好能够提高页面的性能,带来更好的用户体验php

这篇文章也针对重绘重排重渲染从头至尾作一个比较详细的说明:css

  浏览器渲染页面的过程:渲染树相似DOM树,可是并非一一对应的。html

  重绘与重排:重排和重绘代价是高昂的,它们会破坏用户体验,而且让UI展现很是迟缓。java

  哪些状况下会触发重绘与重排;node

  浏览器针对重绘与重排有哪些策略:浏览器会基于你的脚本要求建立一个变化的队列,而后分批去展示。可是有时你的代码会组织浏览器优化重排并当即刷新队列,与此同时展现全部批次的变化。web

  经过减小重排/重绘的负面影响来提升用户体验的最简单方式就是尽量少的去使用他们同时尽量少的请求样式信息。这样浏览器就能够优化重排。由于重绘与重排没法避免,只能尽量少地去使用。文章也给出具体的作法。ajax

  利用工具:Firefox中的MozAfterPaint,但它仅仅可以展示重绘的内容。DynaTrace Ajax和Google发布的SpeedTracer是两个很是棒的深刻了解重绘和重排的工具-前者是IE使用,后者是WebKit。chrome

 

原文:Rendering: repaint, reflow/relayout, restyle浏览器

译文:重绘重排重渲染

渲染过程

  不一样的浏览器工做方式是不同的,下面的图表提供了渲染时的共同实现,一旦他们已经下载好了你页面的代码,多半都会经过浏览器这样实现。

  浏览器把HTML源代码解析,而且建立一个DOM树(DOM tree)-每一个HTML标签在这个树上都有一个对应的节点,标签中的文本也有一个相应的文本节点。DOM树上的根节点是documentElement(也就是<html></html>

  浏览器解析CSS代码,经过一堆hack使得它变得有意义,诸如-moz-webkit和其它不能理解的拓展名,浏览器会大胆的把他们忽略。CSS级联的优先级从低到高是:浏览器默认的基本规则,用户引用的样式表,优先级最高的的是外部引入的而且最终被加入到HTML标签的style属性中的行内样式。

  接下来就是有趣的部分-建立一个渲染树(render tree)。渲染树相似DOM树,可是并非一一对应的。渲染树基于样式,因此若是你使用display:none属性来隐藏一个div,那么它不会被呈如今渲染树中。其它像head标签和全部在其中不可见的元素也同样。另外一方面,DOM元素可能在渲染树中存在多个节点,好比说每行在<p></p>中的文本都须要一个渲染节点。渲染树上的节点被称为一个frame或者一个box(跟CSS box相似,相关内容可查阅the box model)。每个节点都有CSS box的属性-宽度、高度、边框、外边距等等。

  一旦渲染树被建立成功,浏览器就能够在屏幕上绘制渲染树节点。

森林和树

  让咱们举个例子。

  HTML源码:

<html>
<head>
  <title>Beautiful page</title>
</head>
<body>

  <p>
    Once upon a time there was 
    a looong paragraph...
  </p>

  <div style="display: none">
    Secret message
  </div>

  <div></div>
  ...

</body>
</html>

  呈现这个HTML文档的DOM树对每个标签和标签间每一段文本都有一个对应的节点。(为了简便起见,咱们暂且忽略空白符也是文本节点。)

documentElement (html)
    head
        title
    body
        p
            [text node]

        div 
            [text node]

        div
            img

        ...

  渲染树就是DOM树中可见的部分。它缺乏一些元素-好比head和隐藏的div,可是它对文本有额外的节点(又名frame/box)。

root (RenderView)
    body
        p
            line 1
      line 2
      line 3
      ...

  div
      img

  ...

  渲染树的根节点是包含的全部其它元素的frame/box。你能够把它理解为限制在浏览器页面范围内的窗口区域。技术上来讲,WebKit把根节点称为RenderView,对应于CSS的初始化包含块(initial containing block),也就是从视窗范围内页面(0, 0)到(window.innerWidthwindow.innerHeight)的矩形显示区域。

  搞清楚怎样呈现和呈现什么须要递归的遍历渲染树。

重绘和重排

  不管什么时候总会有一个初始化的页面布局伴随着一次绘制。(除非你但愿你的页面是空白的:))以后,每一次改变用于构建渲染树的信息都会致使如下至少一个的行为:

  1. 部分渲染树(或者整个渲染树)须要从新分析而且节点尺寸须要从新计算。这被称为重排。注意这里至少会有一次重排-初始化页面布局。

  2. 因为节点的几何属性发生改变或者因为样式发生改变,例如改变元素背景色时,屏幕上的部份内容须要更新。这样的更新被称为重绘。

  重排和重绘代价是高昂的,它们会破坏用户体验,而且让UI展现很是迟缓。

 

什么状况会触发重排和重绘?

任何改变用来构建渲染树的信息都会致使一次重排或重绘。

  • 添加、删除、更新DOM节点

  • 经过display: none隐藏一个DOM节点-触发重排和重绘

  • 经过visibility: hidden隐藏一个DOM节点-只触发重绘,由于没有几何变化

  • 移动或者给页面中的DOM节点添加动画

  • 添加一个样式表,调整样式属性

  • 用户行为,例如调整窗口大小,改变字号,或者滚动。

让咱们来看一些例子:

var bstyle = document.body.style; // cache

bstyle.padding = "20px"; // 重排+重绘
bstyle.border = "10px solid red"; // 另外一次重排+重绘

bstyle.color = "blue"; // 没有尺寸变化,只重绘
bstyle.backgroundColor = "#fad"; // 重绘

bstyle.fontSize = "2em"; // 重排+重绘

// 新的DOM节点 - 重排+重绘
document.body.appendChild(document.createTextNode('dude!'));

  一些重排可能开销更大。想象一下渲染树,若是你直接改变body下的一个子节点,可能并不会对其它节点形成影响。可是当你给一个当前页面顶级的div添加动画或者改变它的大小,就会推进整个页面改变-听起来代价就十分高昂。

善于应对的浏览器

  既然渲染树变化伴随的重排和重绘代价如此巨大,浏览器一直致力于减小这些消极的影响。一个策略就是干脆不作,或者说至少如今不作。浏览器会基于你的脚本要求建立一个变化的队列,而后分批去展示。经过这种方式许多须要一次重排的变化就会整合起来,最终只有一次重排会被计算渲染。浏览器能够向已有的队列中添加变动并在一个特定的时间或达到一个特定数量的变动后执行。

  可是有时你的代码会组织浏览器优化重排并当即刷新队列,与此同时展现全部批次的变化。这一般发生在你请求样式信息的时候,例如:

1.offsetTop, offsetLeft, offsetWidth, offsetHeight
2.scrollTop/Left/Width/Height
3.clientTop/Left/Width/Height
4.getComputedStyle(), or currentStyle in IE

  以上就是一个节点基本的可请求信息。不管合适你发出请求,浏览器不得不提供给你最新的值。为了实现这一目的,浏览器须要应用全部队列中的变动,刷新队列而后去实现重排。

  举个例子,同时去set和get样式是很糟糕的想法:

// no-no!
el.style.left = el.offsetLeft + 10 + "px";

最小化重排和重绘

  经过减小重排/重绘的负面影响来提升用户体验的最简单方式就是尽量少的去使用他们同时尽量少的请求样式信息。这样浏览器就能够优化重排。如何实践呢?

  • 不要逐个变样式。对于静态页面来讲,明智且兼具可维护性的作法是改变类名而不是样式。对于动态改变的样式来讲,相较每次微小修改都直接触及元素,更好的办法是统一在cssText变量中编辑。
 // bad
    var left = 10,
        top = 10;
    el.style.left = left + "px";
    el.style.top  = top  + "px";

    // better 
    el.className += " theclassname";

    // 当top和left的值是动态计算而成时...

    // better
    el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • “离线”的批量改变和表现DOM。“离线”意味着不在当前的DOM树中作修改。你能够:
  • 经过documentFragment来保留临时变更。
  • 复制你即将更新的节点,在副本上工做,而后将以前的节点和新节点交换。
  • 经过display:none属性隐藏元素(只有一次重排重绘),添加足够多的变动后,经过display属性显示(另外一次重排重绘)。经过这种方式即便大量变动也只触发两次重排。

  • 不要频繁计算样式。若是你有一个样式须要计算,只取一次,将它缓存在一个变量中而且在这个变量上工做。看一下下面这个反例

// no-no!
    for(big; loop; here) {
        el.style.left = el.offsetLeft + 10 + "px";
        el.style.top  = el.offsetTop  + 10 + "px";
    }

    // better
    var left = el.offsetLeft,
        top  = el.offsetTop
        esty = el.style;
    for(big; loop; here) {
        left += 10;
        top  += 10;
        esty.left = left + "px";
        esty.top  = top  + "px";
    }
  • 一般状况下,考虑一下渲染树和变动后须要从新验证的消耗。举个例子,使用绝对定位会使得该元素单独成为渲染树中body的一个子元素,因此当你对其添加动画时,它不会对其它节点形成太多影响。当你在这些节点上放置这个元素时,一些其它在这个区域内的节点可能须要重绘,可是不须要重排。

工具

  就在一年前,尚未什么浏览器工具可以展现绘制和渲染的过程。可是如今已经有了不少很是酷的工具。

  第一个是Firefox中的MozAfterPaint,但它仅仅可以展示重绘的内容。

  DynaTrace Ajax和最近Google发布的SpeedTracer是两个很是棒的深刻了解重绘和重排的工具-前者是IE使用,后者是WebKit。

  去年的某个时候Douglas Crockford发现咱们在CSS中作了一些连咱们本身也不知道的蠢事情。我也深有体会。我以前陷入了一个难题,当在IE6中改变字体大小的时候会致使CPU占用飙升至100%,而且在大概10-15分钟后才能重绘整个页面。

  如今有了这些工具,咱们再也没有借口在CSS中作这些愚蠢的事情。

  除此之外,若是像Firebug这类工具除DOM树之外还展现了渲染树不是很酷吗?

最后一个例子

  让咱们来看看这些工具该而且演示一下restyle(不影响几何形状的渲染树变化)、reflow(重排,影响布局)和repaint(重绘)。

  咱们来比较一下两种方式。第一种,我在不改变布局的状况下改变一些样式,同时在每一个变动以后,咱们检查一下样式属性,这些变动之间彻底没有联系。

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

  一样的变动,可是咱们仅仅在全部变动完成后再查询样式属性的信息:

bodystyle.color = 'yellow';
bodystyle.color = 'pink';
bodystyle.color = 'blue';

tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;

  在这两种状况下,都用到了如下这些变量:

var bodystyle = document.body.style;
var computed;
if (document.body.currentStyle) {
  computed = document.body.currentStyle;
} else {
  computed = document.defaultView.getComputedStyle(document.body, '');
}

  如今两个例子会在document被点击后触发。测试页在这里,咱们称之为“restyle” - restyle.html

  第二个测试和第一个相似,可是此次咱们还改变的布局信息:

// touch styles every time
bodystyle.color = 'red';
bodystyle.padding = '1px';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
bodystyle.padding = '2px';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
bodystyle.padding = '3px';
tmp = computed.backgroundAttachment;

// touch at the end
bodystyle.color = 'yellow';
bodystyle.padding = '4px';
bodystyle.color = 'pink';
bodystyle.padding = '5px';
bodystyle.color = 'blue';
bodystyle.padding = '6px';
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;

  这个测试改变了布局,因此咱们称之为“relayout测试”,源代码在这.

  这是你在restyle测试中用DynaTrace看到的可视化结果。

  页面加载完以后,我点击一次去执行第一个方案(修改完后当即请求样式信息,在大概两秒处),而后再次点击去执行第二方案(样式变动完成后再请求样式信息,在大概四秒处)。

  这个工具为咱们展现了页面是怎样加载的和IE的logo正处于加载状态。而后鼠标光标放置于渲染事件部分。放大后能看到更详细的信息:

  你能够很是清楚的看到蓝色条,表明JavaScript事件,还有下面的绿色条,表明渲染事件。如今,这是个很简单的例子,可是仍然须要注意那些条的长度 - 渲染比执行JavaScript消耗多了多长时间。在Ajax/富应用中,JavaScript不是瓶颈,DOM的访问、操做和渲染才是。

  好了,如今咱们来运行一下"relayout测试",它改变了body的几何形状。此次咱们在“PurePaths”视图中检查。这是一条能够查看每项元素的信息的时间线。我已经高亮了第一次点击,它做为JavaScript事件提供了一个定时的布局任务。

  再一次的放大这个有趣的部分,你能够看到除了“绘制”条之外,还有一个新的“计算流布局”,这是由于在这个测试中,咱们除了重绘一样触发了重排。

  如今让咱们在Chrome中测试同一个页面,而后看看SpeedTracer的结果。

  这是第一个“restyle”测试放大到了有趣的部分,看看发生了什么。

  整体来看就是一次点击和一次绘制。可是在第一次点击的时候,一样有一半的时间消耗在从新计算样式。为何会这样呢?这是由于咱们在每次变动时就请求一次样式信息。

  展开事件会出现隐藏的线(灰色的线因为很快会被Speedtracer隐藏),咱们可以看到发生了什么 - 在第一次点击后,样式被计算了三次。而第二种方案只计算了一次。

 

  如今让咱们运行“relayout测试”。整个列表看起来是同样的:

  可是详细的视图展现了第一次点击后致使了三次重排而第二次点击只致使了一次重排。这很清楚的展现了发生了什么。

  这两个工具备一些微小的不一样 - SpeedTracer不会展现布局任务什么时候被执行和放入队列,而DynaTrace会。DynaTrace不会区分restylereflow/repaint的区别,而SpeedTracer会。可能IE对这二者并无作区分吧。DynaTrace在两次测试中并不会由于重排次数不一样而显示三次重排或者一次,可能IE工做原理本就如此?

  即便运行上述测试几百次,IE浏览器仍然不关心你在改变样式后是否请求样式信息。

  屡次运行这些测试后,如下是得出的结论:

  • 在chrome相较于改变样式后当即请求样式信息,等待样式修改完成后检查,在restyle测试中会快2.5倍,在relayout测试中会快4.42倍。

  • 在Fifefox中,restyle测试快1.87倍,relayout测试快4.64倍。

  • IE6和IE8中,没有什么区别。

  在全部浏览器中改变样式仅仅须要改变样式和布局的一半时间。(虽然我这么说,但实际上我仅仅比较了改变样式和改变布局。)除了在IE6中改变布局会以改变样式四倍的代价来消耗资源。

小结

很是感谢你看完了这篇很长的文章。但愿你们注意到重排这个特性。做为总结,我来从新解释一下术语。

  • 渲染树 - DOM树的可见部分。

  • 渲染树上的节点被称为boxifame

  • 从新计算渲染树被称为reflow(在Firefox中),在其它浏览器中被成为relayout

  • 更新屏幕中的显示结果而且从新计算渲染树被成为repaint或者在IE/DynaTrace中被称为redraw

  • SpeedTracer将没有几何变化的样式变更称为样式重计算,将有几何变更的称为layout

 

若是你想更深层次的去了解这些,能够参考如下资料:

相关文章
相关标签/搜索