[转载] 浏览器渲染Rendering那些事:repaint、reflow/relayout、restyle

原文连接:http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/javascript

转载连接:http://www.cnblogs.com/ihardcoder/p/3927709.htmlphp

我的备注:css

  1. 渲染树的部分或者所有将须要从新构造而且渲染节点的大小须要从新计算。这个过程叫作回流-reflow,或者layout。浏览器中至少存在一个reflow行为-即页面的初始化layout。
  2. 将从新计算后的渲染树更新到屏幕的行为叫作重绘-repaint,或者redraw。要么是由于节点的几何结构改变,要么是由于格式改变,如背景色的变化。
  3. restyle——没有几何结构改变的渲染树变化(而回流reflow会同时影响布局layout)。html

  4. 改变任何影响构造渲染树的行为都会触发重绘repaint,例如java

    1. 增长、删除、更新DOM节点;
    2. 经过display:none隐藏节点会触发重绘和回流,经过visibility:hidden隐藏只会触发重绘,由于没有几何结构的改变;
    3. 移动节点和动画;
    4. 增长、调整样式;
    5. 用户操做行为,如调整窗口大小、改变字体大小、滚动窗口等。
  5. 如何减小重绘repaint和回流reflownode

    1. 不要逐个修改多个样式。对于静态样式来讲,最明智和易维护的代码是经过改变classname来控制样式;而对于动态样式来讲,经过一次修改节点的cssText来代替样式的逐个改变。
    2. "离线"处理多个DOM操做。“离线”的意思是将须要进行的DOM操做脱离DOM树,好比:
      • 经过documentFragment集中处理临时操做;
      • 将须要更新的节点克隆,在克隆节点上进行更新操做,而后把原始节点替换为克隆节点;
      • 先经过设置display:none将节点隐藏(此时出发一次回流和重绘),而后对隐藏的节点进行100个操做(这些操做都会单独触发回流和重绘),完毕后将节点的display改回原值(此时再次触发一次回流和重绘)。经过这种方法,将100次回流和重绘缩减为2次,大大减小了消耗
    3. 不要过多进行重复的样式计算操做。若是你须要重复利用一个静态样式值,能够只计算一次,用一个局部变量储存,而后利用这个局部变量进行相关操做。
    4. 总之,当你在打算改变样式时,首先考虑一下渲染树的机制,而且评估一下你的操做会引起多少刷新渲染树的行为。例如,咱们知道一个绝对定位的节点是会脱离文档流,因此当对此节点应用动画时不会对其余节点产生很大影响,当绝对定位的节点置于其余节点上层时,其余节点只会触发重绘,而不会触发回流。

 

有没有被标题中的5个“R”吓到?今天,咱们来讨论一下浏览器的渲染(Rendering)-一个产生于Page 2.0生命周期中,甚至有时候会在下载瀑布流中出现的过程。web

咱们来讨论浏览器在接收到HTML、CSS和JavasSript后,如何把你的页面呈如今屏幕上。浏览器

1、浏览器渲染过程

不一样的浏览器的渲染过程存在些许不一样,但大致的机制是同样的,下图展现的是浏览器自下载彻底部的代码后的大体流程缓存

  1. 首先,浏览器解析HTML源码构建DOM树,在DOM树中,每一个HTML标签都有对应的节点,而且在介于两个标签中间的文字块也对应一个text节点。DOM树的根节点是documentElement,也就是<html>标签;
  2. 而后,浏览器对CSS代码进行解析,一些当前浏览器不能识别的CSS hack写法(如-moz-/-webkit等前缀,以及IE下的*/_等)将会被忽略。CSS样式的优先级以下:最低的是浏览器的默认样式,而后是经过<link>、import引入的外部样式和行内样式,最高级的是直接写在标签的style属性中的样式;
  3. 随后将进入很是有趣的环节-构建渲染树。渲染树跟DOM树结构类似但并不彻底匹配。渲染树会识别样式,因此若是经过设置display:none隐藏的标签是不会被渲染树引入的。一样的规则适用于<head>标签以及其包含的全部内容。另外,在渲染树中可能存在多个渲染节点(渲染树中的节点称为渲染节点)映射为一个DOM标签,例如,多行文字的<p>标签中的每一行文字都会被视为一个单独的渲染节点。渲染树的一个节点也称为frame-结构体,或者盒子-box(与CSS盒子相似)。每一个渲染节点都具备CSS盒子的属性,如width、height、border、margin等;
  4. 最后,等待渲染树构建完毕后,浏览器便开始将渲染节点一一绘制-paint到屏幕上。

 2、森林和树

首先咱们先看一个例子:app

复制代码
<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><img src="..." /></div>
  ...
 
</body>
</html>
复制代码

HTML结构中的每一个标签和标签间的文字都会被映射为DOM树种的一个节点(实际上,空白区域也会被映射为一个text节点,为了简单说明,在此忽略),构建完成的DOM树结构以下:

复制代码
documentElement (html)
    head
        title
    body
        p
            [text node]
        
        div 
            [text node]
        
        div
            img
        
        ...
复制代码

因为渲染树会忽略head内容和隐藏的节点,而且会将<p>中的多行文字按行数映射为单独的渲染节点,故构建完成的渲染树结构以下:

复制代码
root (RenderView)
    body
        p
            line 1
        line 2
        line 3
        ...
        
    div
        img
        
    ...
复制代码

渲染树的根节点是一个包括全部其余节点的结构体(盒子)。你能够将它理解为浏览器窗口的内部区域(我的理解为可绘制区域,即不包括浏览器边框、菜单栏、标签栏等等),页面被限制在此区域内。严格来讲,webkit将渲染树的根节点称为渲染视图-RenderView,渲染视图符合CSS初始包含块-initial containing block,也就是浏览器的整个可绘制区域,从坐标(0,0)到(window.innerWidth,window.innerHeight)。

接下来,咱们将研究浏览器是如何经过循环遍历渲染树把页面展现到屏幕上的。

3、重绘-repaint和回流-reflow

同一时间内至少存在一个页面初始化layout行为和一个绘制行为(除非你的页面是空白页-blank)。在此以后,改变任何影响构造渲染树的行为都会触发如下一种或者多种动做:

  1. 渲染树的部分或者所有将须要从新构造而且渲染节点的大小须要从新计算。这个过程叫作回流-reflow,或者layout,或者layouting(靠,能不能愉快的翻译了,是否是还来个过去式啊?!),或者relayout(这词是原文做者杜撰的,为了标题中多个“R”)。浏览器中至少存在一个reflow行为-即页面的初始化layout;
  2. 屏幕的部分区域须要进行更新,要么是由于节点的几何结构改变,要么是由于格式改变,如背景色的变化。屏幕的更新行为称做重绘-repaint,或者redraw。

重绘和回流的性能消耗是很是严重的,破坏用户体验,形成UI卡顿。

4、触发重绘/回流的机制

改变任何影响构造渲染树的行为都会触发重绘,例如

  1. 增长、删除、更新DOM节点;
  2. 经过display:none隐藏节点会触发重绘和回流,经过visibility:hidden隐藏只会触发重绘,由于没有几何结构的改变;
  3. 移动节点和动画;
  4. 增长、调整样式;
  5. 用户操做行为,如调整窗口大小、改变字体大小、滚动窗口(OMG,no!)等。

举个栗子:

复制代码
var bstyle = document.body.style; // 缓存
 
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节点的渲染树,若是你在此渲染树中乱搞,它不会影响不少其余节点(这个长句翻译很差,原文以下:Think of the render tree - if you fiddle with a node way down the tree that is a direct descendant of the body, then you're probably not invalidating a lot of other nodes)。可是若是将页面顶部的一个div作动画或改变尺寸,页面的其余部分会被挤来挤去,这听起来会消耗不少性能。

5、聪明的浏览器

浏览器一直在努力减小消耗巨大的重绘和回流行为。要么选择不执行,要么至少不当即执行。浏览器会生成一个队列用于缓存这些行为而且以块为单位执行它们。经过这种方法,屡次引起重绘或回流的操做会被组合在一块儿,以便在一个回流中完成。浏览器将这些操做加入到缓存队列中,当到达必定的时间间隔,或者累积了足够多的操做行为后执行它们。

可是,有时候某些的代码会破坏上述的浏览器优化机制,致使浏览器刷新缓存队列而且执行全部已已缓存的操做行为。这种状况发生在请求/获取下面这些样式的行为中:

  1. offsetTop,offsetLeft,offsetWidth,offsetheight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. getComputedStyle(),或者IE下的currentStyle

以上的行为本质上是获取一个节点的样式信息,浏览器必须提供最新的值。为了达到此目的,浏览器须要将缓存队列中的全部行为所有执行完毕,而且被强制回流。

因此,在一条逻辑中同时执行set和get样式操做时很是很差的,以下:

el.style.left = el.offsetLeft + 10 + "px";

6、如何减小重绘和回流

减小由于重绘和回流引发的糟糕用户体验的本质是尽可能减小重绘和回流,减小样式信息的set行为。能够经过如下几点来优化:

  1. 不要逐个修改多个样式。对于静态样式来讲,最明智和易维护的代码是经过改变classname来控制样式;而对于动态样式来讲,经过一次修改节点的cssText来代替样式的逐个改变。
    复制代码
    // 糟糕的办法
    var left = 10,
        top = 10;
    el.style.left = left + "px";
    el.style.top  = top  + "px";
     
    //静态样式经过改变classname
    // better 
    el.className += " theclassname";
     
    // 动态样式统一修改cssText
    // better
    el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
    复制代码
  2. "离线"处理多个DOM操做。“离线”的意思是将须要进行的DOM操做脱离DOM树,好比:
    • 经过documentFragment集中处理临时操做;
    • 将须要更新的节点克隆,在克隆节点上进行更新操做,而后把原始节点替换为克隆节点;
    • 先经过设置display:none将节点隐藏(此时出发一次回流和重绘),而后对隐藏的节点进行100个操做(这些操做都会单独触发回流和重绘),完毕后将节点的display改回原值(此时再次触发一次回流和重绘)。经过这种方法,将100次回流和重绘缩减为2次,大大减小了消耗
  3. 不要过多进行重复的样式计算操做。若是你须要重复利用一个静态样式值,能够只计算一次,用一个局部变量储存,而后利用这个局部变量进行相关操做。例如:
    复制代码
    //糟糕的作法
    for(big; loop; here) {
        el.style.left = el.offsetLeft + 10 + "px";
        el.style.top  = el.offsetTop  + 10 + "px";
    }
     
    //优化后的代码
    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";
    }
    复制代码
  4. 总之,当你在打算改变样式时,首先考虑一下渲染树的机制,而且评估一下你的操做会引起多少刷新渲染树的行为。例如,咱们知道一个绝对定位的节点是会脱离文档流,因此当对此节点应用动画时不会对其余节点产生很大影响,当绝对定位的节点置于其余节点上层时,其余节点只会触发重绘,而不会触发回流。

7、工具

(废话就不翻译了,大概就是一些吐槽IE开发者工具的话)

如今(原文做于2009年12月)有不少能够帮助咱们深刻了解浏览器重绘和回流机制的工具。

  • FireFox提供了mozAfterPaint接口可供开发者查看重绘的动做;
  • DynaTrace Ajax适用于IE浏览器,谷歌的SpeedTracer适用于Webkit内核的浏览器,这两种工具能够帮助开发者深刻挖掘重绘和回流行为;

Douglas Crockford去年提到,咱们可能会对一些不太了解的CSS作一些愚蠢的事情,而且我被包括在内。我被引入了一个项目组,研究一种奇怪的现象:在IE6浏览器中增大font-size会引发CPU占用率到达100%,而且会持续10到15分钟,IE浏览器才会完成重绘行为。

有了工具的辅助,咱们没有任何理由再作一些愚蠢的CSS操做了。

顺便提一句,若是有一种像Firebug的工具能够象查看DOM结构同样查看渲染树,是否是很cooooooooooooooool?

8、举个栗子

 下面咱们简单的看一个如何运用工具来证实restyle(没有几何结构改变的渲染树变化)和回流(同时影响布局layout)、重绘。

第一个测试,咱们比较解决同一问题的两种方法。第一种方法,改变一些样式,在每次改变以后检查一次呗改变的样式。

复制代码
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, '');
}
复制代码

 

上面两中方法的样式改变经过click事件触发。测试页面-restyle.html(点击“dude”)。咱们将第一个测试称为restyle测试。

第二个测试在第一个测试的基础上,同事改变影响布局的样式。

复制代码
// 每次修改后都检查
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;
 
 
// 所有修改完毕后再检查
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测试,测试页面请点击

咱们经过DynaTrace工具获得restyle测试的表现以下图:

等页面加载完毕后,在第2秒左右点击触发第一种方案(即每次修改样式后当即检查),而后在第4秒左右再次点击触发第二种方案(即等待全部样式修改完毕后再统一检查)。

 DynaTrace工具会显示页面的加载过程,从上图能够看到IE的logo图标被加载的时间节点。把鼠标移至Rendering一行以便追踪点击事件,滑动滚轮放大想要追踪的区域能够查看详细信息,以下图:

从上图中能够清晰的看到表明JavaScript行为的蓝色柱形条,一届表明渲染行为的绿色柱形条。经过这个简单的实验,咱们能够注意到两个柱形条的长度,也就是比较渲染行为比JavaScript行为多花费的时间。在Ajax以及富应用中,性能瓶颈并非JavaScript行为,而是DOM节点的操做使用和渲染行为。

接下来咱们来运行relayout测试,也就是涉及几何结构改变的操做行为。经过测试工具的“PurePaths”视图,查看每种行为执行时间的瀑布流。下图中高亮部分显示的是第一次点击事件,执行一段JavaScript逻辑实现一些layout操做。

以下图所示,咱们能够看到在此次的测试中,除了与第一次测试一样的具备表明“绘图”的绿色柱形条之外,还有一个新增的区域-“计算布局流”,由于此次测试中同时触发了重绘和回流。

接下来,咱们经过SpeedTracer工具在Chrome下运行上面两个测试。

第一个测试-restyle测试的运行结果以下图所示:

总的来讲,仍然是一次点击触发一次重绘,可是咱们注意到,在第一次点击的时候,会有50%的时间消耗在计算样式(Style Recalculation)上。致使这种结果的缘由是咱们在每次改变样式后都检查了一次样式信息。

展开事件详细信息后能够清晰的看到,在第一次点击事件后,样式被计算了3次。而第二次点击值计算了一次。以下图所示:

接下来运行第二个测试-relayout测试。整体事件信息与restyle测试大体相同:

可是详情页显示的信息能够看到第一次点击后触发了3次回流(由请求样式信息操做触发),第二次点击只触发了一次回流。经过本工具能够清晰的看到浏览器内部到底发生了什么。

上述两种工具的区别在于:DynaTrace会显示layout行为被执行和加入执行队列的详细时间,而SpeedTracer不会;SpeedTracer会将restyle与reflow/layout两种浏览器行为区别开,而DynaTrace不会。难道IE浏览器自己不会区分这两种行为?另外,在两种不一样的逻辑测试-改变-最后检查(change-end-touch)与改变-当即检查(change-then-touch)中,DynaTrace并不会显示二者触发回流的次数不一样(第一种之触发一次,第二次触发3次,而DynaTrace统一显示为一次),难道IE浏览器的工做机制本就如此?

即便运行上述测试几百次,IE浏览器仍然不关心你在改变样式后是否请求样式信息。(译者注:我彷佛感到原文做者对IE满满的恶意...)

在屡次运行上述测试后,获得几点结论以下:

  1. Chrome中,相比较改变样式后当即检查样式信息,等待所有样式修改完毕后统一检查,在restyle测试中会快2.5倍,relayout测试中快4.42倍;
  2. Firefox中,restyle测试快1.87倍,relayout测试快4.64倍;
  3. IE6和IE8,不要在乎这些细节(呵呵)

在全部浏览器(IE系列不在“全部”的范畴)的测试结果显示,只修改样式的时间花销仅仅是同时改变样式和触发layout的一半(我本该对比只改变样式和只改变layout的时间的,可是我没有,不用谢)。顺便提一下IE6,它的layout时间花销是只改变样式的4倍。(呵呵)

9、总结

很是感谢各位对这篇文章的支持。但愿各位能经过运动上文提到的测试工具改善工做,而且时刻注意回流的触发操做。最后,咱们复习一下几个术语:

  1. 渲染树-DOM树的虚拟部分;
  2. 渲染树中的节点称为结构体或者盒子;
  3. 从新计算渲染树的行为被Mozilla称为回流-reflow,被其余浏览器称为layout;
  4. 将从新计算后的渲染树更新到屏幕的行为叫作重绘-repaint,或者redraw(in IE/DynaTrace);
  5. SpeedTracer会将“计算样式-style recalculation”和“布局-layout”区分开。

扩展阅读,前三篇对浏览器内部机制研究比较深刻,推荐:

相关文章
相关标签/搜索