原文链接:http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/
有没有被标题中的5个“R”吓到?今天,我们来讨论一下浏览器的渲染(Rendering)-一个产生于Page 2.0生命周期中,甚至有时候会在下载瀑布流中出现的过程。
我们来讨论浏览器在接收到HTML、CSS和JavasSript后,如何把你的页面呈现在屏幕上。
一、浏览器渲染过程
不同的浏览器的渲染过程存在些许不同,但大体的机制是一样的,下图展示的是浏览器自下载完全部的代码后的大致流程
二、森林和树
首先我们先看一个例子:
<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)。
接下来,我们将研究浏览器是如何通过循环遍历渲染树把页面展示到屏幕上的。
三、重绘-repaint和回流-reflow
同一时间内至少存在一个页面初始化layout行为和一个绘制行为(除非你的页面是空白页-blank)。在此之后,改变任何影响构造渲染树的行为都会触发以下一种或者多种动作:
重绘和回流的性能消耗是非常严重的,破坏用户体验,造成UI卡顿。
四、触发重绘/回流的机制
改变任何影响构造渲染树的行为都会触发重绘,例如
举个栗子:
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做动画或改变尺寸,页面的其他部分会被挤来挤去,这听起来会消耗很多性能。
五、聪明的浏览器
浏览器一直在努力减少消耗巨大的重绘和回流行为。要么选择不执行,要么至少不立即执行。浏览器会生成一个队列用于缓存这些行为并且以块为单位执行它们。通过这种方法,多次引发重绘或回流的操作会被组合在一起,以便在一个回流中完成。浏览器将这些操作加入到缓存队列中,当到达一定的时间间隔,或者累积了足够多的操作行为后执行它们。
但是,有时候某些的代码会破坏上述的浏览器优化机制,导致浏览器刷新缓存队列并且执行所有已已缓存的操作行为。这种情况发生在请求/获取下面这些样式的行为中:
以上的行为本质上是获取一个节点的样式信息,浏览器必须提供最新的值。为了达到此目的,浏览器需要将缓存队列中的所有行为全部执行完毕,并且被强制回流。
所以,在一条逻辑中同时执行set和get样式操作时非常不好的,如下:
el.style.left = el.offsetLeft + 10 + "px";
六、如何减少重绘和回流
减少因为重绘和回流引起的糟糕用户体验的本质是尽量减少重绘和回流,减少样式信息的set行为。可以通过以下几点来优化:
七、工具
(废话就不翻译了,大概就是一些吐槽IE开发者工具的话)
现在(原文作于2009年12月)有很多可以帮助我们深入了解浏览器重绘和回流机制的工具。
Douglas Crockford去年提到,我们可能会对一些不太了解的CSS做一些愚蠢的事情,并且我被包括在内。我被引入了一个项目组,研究一种奇怪的现象:在IE6浏览器中增大font-size会引起CPU占用率到达100%,并且会持续10到15分钟,IE浏览器才会完成重绘行为。
有了工具的辅助,我们没有任何理由再做一些愚蠢的CSS操作了。
顺便提一句,如果有一种像Firebug的工具可以象查看DOM结构一样查看渲染树,是不是很cooooooooooooooool?
八、举个栗子
下面我们简单的看一个如何运用工具来证明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满满的恶意...)
在多次运行上述测试后,得到几点结论如下:
在所有浏览器(IE系列不在“所有”的范畴)的测试结果显示,只修改样式的时间花销仅仅是同时改变样式和触发layout的一半(我本该对比只改变样式和只改变layout的时间的,但是我没有,不用谢)。顺便提一下IE6,它的layout时间花销是只改变样式的4倍。(呵呵)
九、总结
非常感谢各位对这篇文章的支持。希望各位能通过运动上文提到的测试工具改善工作,并且时刻注意回流的触发操作。最后,我们复习一下几个术语:
扩展阅读,前三篇对浏览器内部机制研究比较深入,推荐: