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

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

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

一、浏览器渲染过程

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

  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到屏幕上。

二、森林和树

首先我们先看一个例子:

<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)。在此之后,改变任何影响构造渲染树的行为都会触发以下一种或者多种动作:

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

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

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

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

  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做动画或改变尺寸,页面的其他部分会被挤来挤去,这听起来会消耗很多性能。

五、聪明的浏览器

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

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

  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";

六、如何减少重绘和回流

减少因为重绘和回流引起的糟糕用户体验的本质是尽量减少重绘和回流,减少样式信息的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. 总之,当你在打算改变样式时,首先考虑一下渲染树的机制,并且评估一下你的操作会引发多少刷新渲染树的行为。例如,我们知道一个绝对定位的节点是会脱离文档流,所以当对此节点应用动画时不会对其他节点产生很大影响,当绝对定位的节点置于其他节点上层时,其他节点只会触发重绘,而不会触发回流。

七、工具

(废话就不翻译了,大概就是一些吐槽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?

八、举个栗子

下面我们简单的看一个如何运用工具来证明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倍。(呵呵)

九、总结

非常感谢各位对这篇文章的支持。希望各位能通过运动上文提到的测试工具改善工作,并且时刻注意回流的触发操作。最后,我们复习一下几个术语:

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

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

【翻译】浏览器渲染Rendering那些事:repaint、reflow/relayout、restyle的更多相关文章

  1. 【Web动画】CSS3 3D 行星运转 && 浏览器渲染原理

    承接上一篇:[CSS3进阶]酷炫的3D旋转透视 . 最近入坑 Web 动画,所以把自己的学习过程记录一下分享给大家. CSS3 3D 行星运转 demo 页面请戳:Demo.(建议使用Chrome打开 ...

  2. 浏览器渲染页面原理,reflow、repaint及其优化

    浏览器的主要组件包括: 1.      用户界面 - 包括地址栏.前进/后退按钮.书签菜单等.除了浏览器主窗口显示的你请求的页面外,其他显示的各个部分都属于用户界面. 2.      浏览器引擎 - ...

  3. 理解浏览器的重绘与回流(repaint&&reflow)

    今天在做练习的时候,遇到了重绘与回流这个词,表示连个毛都没有听过.遂查之,首先将网上的(http://blog.sina.com.cn/s/blog_8dace7290102wezv.html)关于这 ...

  4. Web标准的简单理解 不同内核浏览器的差异以及浏览器渲染简介(转)

    Web标准是一系列标准的集合.这些标准大概分三方面:结构.表现和行为.结构化主要有HTML, XHTML和XML,表现主要有CSS,行为标准主要包括对象模型,如 W3C DOM.ECMAScript等 ...

  5. 浏览器渲染流程&Composite(渲染层合并)简单总结

    梳理浏览器渲染流程 首先简单了解一下浏览器请求.加载.渲染一个页面的大致过程: DNS 查询 TCP 连接 HTTP 请求即响应 服务器响应 客户端渲染 这里主要将客户端渲染展开梳理一下,从浏览器器内 ...

  6. CSharpGL(31)[译]OpenGL渲染管道那些事

    CSharpGL(31)[译]OpenGL渲染管道那些事 +BIT祝威+悄悄在此留下版了个权的信息说: 开始 自认为对OpenGL的掌握到了一个小瓶颈,现在回头细细地捋一遍OpenGL渲染管道应当是一 ...

  7. 转:JavaScript定时机制、以及浏览器渲染机制 浅谈

    昨晚,朋友拿了一道题问我: a.onclick = function(){ setTimeout(function() { //do something ... },0); }; //~~~ 我只知道 ...

  8. 从敲入 URL 到浏览器渲染完成、对HTTP协议的理解

    1. 大致过程 当你这样子回答的时候: 用户输入 url 地址,浏览器查询 DNS 查找对应的请求 IP 地址 建立 TCP 连接 浏览器向服务器发送 http 请求,如果服务器段返回以 301 之类 ...

  9. (前端常考面试题)从敲入 URL 到浏览器渲染完成,到底发生了什么 ?

    前言 小汪最近在看[WebKit 技术内幕]一书,说实话,这本书写的太官方了,不通俗易懂. 但是看完书,对浏览器内核的 WebKit 有了进一步的了解,所以从浏览器内核出发,写这篇文章以记录学到的知识 ...

随机推荐

  1. android shape 怎么在底部画横线

    使用layer-list可以,画了两层 1 2 3 4 5 6 7 8 9         <layer-list>             <!-- This is the lin ...

  2. Win10下 usart驱动PL2303无法安装的问题

    随着系统的 普及,很多小伙伴也放弃了原有的win7系统,加入了win10的行列.但是相对win7的稳定来说,win10还存在很多的不足 . 新买了一个usart的模块,但是在自家的电脑上使用的时候 一 ...

  3. [C#.net]获取文本文件的编码,自动区分GB2312和UTF8

    昨天生产突然反馈上传的结果查询出现了乱码,我赶紧打开后台数据库,发现果真有数据变成了乱码.这个上传程序都运行3个多月了,从未发生乱码现象,查看程序的运行日志,发现日志里的中文都变成了乱码,然后对比之前 ...

  4. flag:用心学习的第一天

    目标是:加油学习,尽早改变世界

  5. Effective Java 【考虑实现Comparable接口】

    Effective Java --Comparable接口 compareTo方法是Comparable接口的唯一方法.类实现了Comparable接口,表明它的实例具有内在的排序关系. 自己实现co ...

  6. 学以致用三十四-----python2.0加载图片

    想用做一个静态图片为背景的页面.结果遇到了一些阻碍.其主要原因还是路径没有找对.网上也参考了不少方法,也许是因为版本不同,处理的方法也不同,因此按照网上的处理方式,也没有得到解决. 为此困惑了一天.结 ...

  7. linux系统中使用socket直接发送ARP数据

    这个重点是如这样创建socket:  sock_send = socket ( PF_PACKET , SOCK_PACKET , htons ( ETH_P_ARP) ) ; 其后所有收发的数据都是 ...

  8. C++的IO处理中的头文件以及类理解(1)

    C++语言不直接处理输入输出,而是通过一簇定义在标准库中的类型来处理IO.这些类型支持从设备读取数据.向设备写入数据的IO操作,设备可以是文件.控制台窗口等,还有一些类型允许内存IO,即,从strin ...

  9. [转] 分代垃圾回收的 新旧代引用问题(原标题:Back To Basics: Generational Garbage Collection)

    原文链接: https://blogs.msdn.microsoft.com/abhinaba/2009/03/02/back-to-basics-generational-garbage-colle ...

  10. 架构(三)MongoDB安装配置以及集群搭建

    一 安装 1.1 下载MongoDB 我个人不太喜欢用wget url, 之前出现过wget下载的包有问题的情况 https://fastdl.mongodb.org/linux/mongodb-li ...