3

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

原文链接:Rendering: repaint, reflow/relayout, restyle

正文:

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

渲染过程:

不同的浏览器有不同的实现方式,但是下边的图会展示一个当代码被下载到电脑里之后所有浏览器的共同实现(或多或少都有)
图片描述

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

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

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

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

森林&树

举个例子:

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


已注销
214 声望5 粉丝

写代码不要局限某种语言,解决问题才是最重要的