【译】浏览器渲染:repaint,reflow/relayout,restyle

**首先说翻译这篇文章的目的实际上是,以前回答的关于浏览器js渲染的问题被打脸了 ಥ_ಥ ,
不得不正视本身半路出家学前端的事实,因此这篇文章就算是本身的一个笔记吧,学而时习之,不亦乐乎,翻译错了,还请批评指正**javascript

原文连接:Rendering: repaint, reflow/relayout, restylephp

正文:css

今天咱们来谈谈这个从page2.0(译者注:page2.0不怎么常见,应该是做者自创的,介绍连接在文章结尾)生命周期的词:渲染,有时候它甚至发生在瀑布流当中。
那么,浏览器是如何靠着一大块html,css,javascript代码在屏幕上显示你的页面的呢?html

渲染过程:

不一样的浏览器有不一样的实现方式,可是下边的图会展现一个当代码被下载到电脑里以后全部浏览器的共同实现(或多或少都有)
图片描述前端

  • 浏览器把咱们的html源代码解析而且初始化成一个dom树,dom树是一个数据结构,它的特色包括,每一个html标签都在这个树上有一个对应的节点,固然标签当中的文本块也在dom树上有一个相应的文本节点,这个dom树的根节点就是documentElement(也就是<html></html>标签)java

  • 浏览器解析css代码,针对一些像-webkit、-moz以及一些不认识的写法作忽略,css的优先级是这样的:最基本的浏览器默认样式,而后就是来自外部引入的用户脚本,最高级的是在页面里边写在<style>标签里面的样式node

  • 接下来就是有意思的部分:初始化一个渲染树。渲染师有点相似于dom树,可是并非彻底相同的。渲染树知道样式(style),因此若是你用display:none来隐藏一个div,那么这个div并不会在渲染树上被引入,其余的一些不可见的好比head也是同样的道理。另外一方面,一个dom节点在渲染树上可能对应多个节点,好比说文本节点,<p>标签每一行都须要一个渲染节点。在渲染树上的一个节点被称为一个frame,或者是一个box(跟css box相似),每一个渲染树节点都有css box属性-width、height、border、margin以及其余。web

  • 一旦渲染树初始化完成,浏览器就能够在屏幕上开始绘制节点。ajax

森林&树

举个例子:浏览器

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

基本上来讲,dom树映射html源码的方式是一个节点对应一个标签,一个文本节点对应一个标签里的文字(简单起见忽略空白也对应文本节点的事实)

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

渲染树会成为dom树的可见部分,它抛弃了一些东西-head里的以及hidden起来的div,可是它会为文本的每一行产生另外的节点(aka frames, aka boxes)。

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

渲染树的根节点会包括全部的其余节点,因此你能够把根节点当成浏览器窗口的区域,页面被限制在这里,技术上webkit把根节点称做renderView而且对应css当中的initial containing block(初始化包含块),也就是从page(0,0)到(window.width,window.height)的矩形显示区。
接下来递归遍历整个渲染树来了解什么东西以及如何让这些显示在屏幕上

Repaints and reflows(重绘以及从新布局)

无论何时至少会有一个初始化页面布局和一个绘制行为(原文:There's always at least one initial page layout together with a paint )(除非你的页面是空白的),以后,若是改变构造渲染树信息都会触发下边一种或两种结果:

  1. 渲染树的一部分或者整棵树都会被从新分析而且节点尺寸被从新计算,这就被称为reflow或者是layout又或者是layouting。注意至少会有一个reflow,即页面的初始化布局。

  2. 屏幕的部分区域须要更新,有多是节点的形状变化也有多是样式变了好比背景颜色。屏幕的变化就被称为repaint或者是redraw。

不论是reflow仍是repaint都是很耗费资源的事,它们会损害用户体验以及让ui显得卡顿

什么会触发reflow或者repaint

任何改变用于构建渲染树的初始化信息的都会触发reflow或者repaint,好比:

  • 添加、删除、改变dom节点

  • 经过display: none隐藏一个节点(触发reflow and repaint)以及经过visibility: hidden隐藏节点(仅触发repaint)

  • 移动或者给一个节点添加动画

  • 添加一个样式,或者调整样式

  • 用户操做好比调整窗口大小,改变字体大小,或者滚动等

看个例子:

var bstyle = document.body.style; // cache
 
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // another reflow and a repaint
 
bstyle.color = "blue"; // repaint only, no dimensions changed
bstyle.backgroundColor = "#fad"; // repaint
 
bstyle.fontSize = "2em"; // reflow, repaint
 
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));

有些reflow会有更大的开销,试想一个渲染树,若是有一个body下的直接子节点,对他胡乱操做可能并不会影响其余的节点,可是当你操做一个顶部的div改变他的尺寸或者加动画,以此来推进其余部分,这听起来就很费资源

身经百战的浏览器

reflow、repaint跟渲染树的联系起来使得开销变大。而浏览器的目标之一就是减小reflow以及repaint的负面影响,其中的一个策略就是干脆不作,又或者说至少不是如今作。浏览器会设置一个队列来收集这些要改变渲染树或屏幕的动做,而且分批执行,这样每个须要一系列reflow的变化会被整合在一块儿,因此最终只有一个reflow须要被计算分析。浏览器能够把这些变化添加在队列当中而后一旦到达定时器时间或者必定数量的操做就开始(刷新)执行。

但有时脚本语句会破化浏览器优化reflow,并使其刷新队列以及执行全部批处理的改变。这些发生在你请求样式信息,好比:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight

  2. scrollTop/Left/Width/Height

  3. clientTop/Left/Width/Height

  4. getComputedStyle(), 或者在IE当中的currentStyle

全部这些上述的都是关于一个节点基本的请求样式信息,无论何时请求,浏览器只能给你一个最精确的值,所以浏览器须要将队列中的全部行为所有执行完毕,而且强制reflow.
好比说,一句话处理set和get样式信息就不是个好的实践

// 不要这么作
el.style.left = el.offsetLeft + 10 + "px";

尽可能减小repaints 以及 reflows

减小reflows以及repaint对用户体验的负面影响根本上来讲是减小reflow以及repaint的次数以及减小对样式信息的请求,这样浏览器就能够优化reflows,可是如何去作呢?

  • 不要试图逐个的的改变初始样式,对静态页面来讲,明智而且可维护的作法是改变class的名字,而不是一个接一个改样式。对动态的样式来讲,修改csstext要比直接接触元素修改样式要好。

// 不要这么作
var left = 10,
    top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";
 
// 好的作法
el.className += " theclassname";
 
// or when top and left are calculated dynamically...
 
// 好的作法
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • 离线批量处理dom改变,离线意味着不要在当前的dom树中操做,你能够:
    1 经过documentFragment处理临时操做
    2 把你即将进行操做的节点复制出来,操做副本,而后替换即将要操做的节点
    3 用display:none把节点藏起来(一次reflow,一次repaint),添加完大量的操做,从新展现(一次reflow,一次repaint),用这种方式你能够只渲染两次,大大减小渲染次数

  • 不要频繁的访问样式计算,若是有须要对一个样式进行屡次计算,只作一次,把它缓存到一个变量当中,对本地这个变量进行操做,下边看一个实践:

// 别这么作
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";
}
  • 总之,当你作完操做以后请想一想渲染树以及耗费的资源,好比说使用绝对定位的元素,若是你对它作动画操做不会影响其余太多的元素,当你把这个节点放在其余节点的最上面的时候,这个时候这个区域只须要repaint,而不须要reflow

一个最后的例子

让咱们简单的用一个工具来看看restyle(不影响几何形状的渲染树的变化)、reflow(影响布局)以及repaint的区别:
咱们首先比较一个作一件事的两个方式,第一种方式,咱们改变一些样式(跟布局无关的),每一步作完以后,咱们检查一次属性变化,每步变化之间没有联系

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

第二种方式,咱们再改变完以后再获取style的属性

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 test

第二个测试跟第一个同样,可是咱们同时会改变布局信息:

// 改一步检查一步
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 test”,source

经过DynaTrace工具咱们看到restyle的可视化信息:
图片描述

页面加载完以后,我点击了一次来执行第一方案(改完以后马上检查,在大约第二秒处),而后点击第二种方案(所有改完以后才检查,大约四秒处)
工具显示了页面加载的方式而且能够看到那个IElogo显示正在加载,而后将鼠标光标在下面rendering以显示事件。滚轮放大看更多细节:
图片描述

咱们看到蓝色的javascript条以及绿色的rendering条,咱们注意到条的长度,渲染比js执行要耗费更多的时间,在ajax及其复杂应用当中,js行为不是应用瓶颈,dom的访问、操纵、执行才是。
好如今咱们来跑一下relayout test,改变body几何形状,这一次看看"PurePaths"这个面板,查看每一项的执行时间线,下图中高亮部分显示的是第一次点击事件,执行一段JavaScript逻辑实现一些layout操做。
图片描述

再次,放大到了有趣的部分,你能够看到,如今除了“绘制”吧,还有一个新的 - 在“计算流布局”,由于在这个测试中,咱们除了repaint以外也reflow了
图片描述

如今,让咱们来测试在同一页在Chrome中,并期待在Speed​​Tracer结果。

这是第一个“restyle”试放大到了有趣的部分,看看发生了啥。
图片描述

整体上就是点击一次绘制一次,可是在第一次点击上,有50%的时间花在从新计算样式。其实这是由于每次样式改变都须要询问样式信息

放大事件并显示隐藏线(灰线是由Speed​​tracer隐藏,由于他们很快),咱们能够清楚地看到发生了什么事 - 第一次点击后,样式计算三次。第二次以后 - 只有一次计算。
图片描述

如今咱们来看看relayout test,事件的整个列表看起来是同样的:
图片描述

可是详细视图显示了第一次点击致使了三次reflow(由于须要询问样式信息),第二次只致使了一次reflow,如今咱们知道是怎么回事了
图片描述

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

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

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

Chrome中,相比较改变样式后当即检查样式信息,等待所有样式修改完毕后统一检查,在restyle测试中会快2.5倍,relayout测试中快4.42倍;
Firefox中,restyle测试快1.87倍,relayout测试快4.64倍;
IE6和IE8,无所谓了
在全部的浏览器当中改变样式只花费同时改变布局和样式一半的开销,除了IE,IE当中改变布局要比只改变样式多四倍的花销

总结

谢谢你看完这个长长的帖子,但愿你们注意这些reflows,做为总结,我再次解释一下术语:

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

  • 渲染树上的节点被称为frame或者boxes

  • 渲染树的从新计算被称为reflow(火狐当中),在其余浏览器被称为relayout

  • 将从新计算后的渲染树更新到屏幕的行为叫作repaint,或者redraw(IE当中)

译者的一堆问题(之后补充,译者懒癌发做了):
page2.0介绍

相关文章
相关标签/搜索