【译】理解Repaint和Reflow

3,411 阅读6分钟

最近,在研究为啥React的虚拟dom那么快的时候,我意识到我们不太了解javascript的性能。所以我写这篇文章以帮助提高对Repaint,Reflow,JavaScript性能的认识。

Reapint & Reflow

在我们深入之前,我们知道浏览器是怎么工作的么?

一图胜千言。所以,我们从较高层面来看下浏览器的工作原理!

high-level view

hmm...“browser engine(浏览器引擎)”和“rendering enging(渲染引擎)”是啥?

浏览器引擎的主要工作是把HTML文档和网页的其它资源转换成用户可交互可视化的界面。 除了“browser engine”,还有另外常用的两个术语:“layout engine(布局引擎)” 和 “rendering enging(渲染引擎)”。理论上,布局和渲染处理的引擎可以单独分开。不过实际上,它俩彼此紧密耦合所以不太会分开来说。

让我们了解一下浏览器是如何在屏幕上画出用户界面的

当你点击或输入某个url,浏览器会向该页面发起一个http请求,并且相应的服务器返回HTML文档。(当然中间还有很多细节会发生)

Step by step processing

  • 浏览器解析出HTML源代码并构建一棵DOM树,这棵DOM树就是HTML节点的数据层表现,在这棵树里每个HTML标签都有一个对应的节点,标签之间的文本也会得到一个text node(文本节点)。这课DOM树的根节点就是documentElement<html>标签)

  • 浏览器解析CSS代码。样式信息级联:基本(样式)规则在用户代理样式表中(浏览器默认值),接着可能是用户样式表,网页作者的样式表 - 外部的,导入的,内联的(就是把样式写在HTML标签的属性里)。

  • 接下来是有趣的部分 - 构建渲染树。渲染树有点像DOM树,但是不完全是。渲染树知道样式,例如你用display:none来隐藏一个div的时候,它将不会在渲染树中表示。其它看不见的元素也一样,比如head及在它里面的所有内容。另一方面,渲染树中可能存在用多个节点来表示(一个)DOM元素 - 比如文本节点,例如<p>节点里每一行都需要一个渲染节点。渲染树里的一个节点被叫做box盒子模型)。每个(渲染)节点都一个css盒子属性 - width, height, border, margin,,等。

  • 渲染树建立好以后,浏览器就能把渲染树节点画到屏幕上。

这里有个短视频,浏览器是如果在屏幕上画节点的。 视频-画的动态效果

看不了的同学,我这里放一张静态图,感受一下:

render图

(视频里的过程)我们可能根本就感觉不到,发生在几分之一秒内。

仔细看。

浏览器是如何布局,如何检测根节点,兄弟节点以及孩子节点,以及相应地重新排列他们的布局。

让我们举个例子

<html>
<head>
  <title>Repaint And Reflow</title>
</head>
<body>
    
  <p>
    <strong>How's The Josh?</strong>
    <strong><b> High Sir...</b></strong>
  </p>
  
  <div style="display: none">
    Nothing to display
  </div>
  
  <div><img src="..." /></div>
  ...
 
</body>
</html>

DOM树里基本每个标签都有一个节点对应,节点之间的每个文本都有一个文本节点(这里简单起见,让我们忽略一下,其实一个空白(比如换行)也是一个文本节点)。

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

而渲染树是DOM树的可视部分。不过他会少一点东西-head和hidden的div,不过也有额外复数行的文本节点。

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

渲染树的根节点包含了所有元素的。你可以认为(这个根节点)就是浏览器里面的那部分。WebKit把这个根节点称作RenderView并把它与css里的初始包含块对应起来,也就是页面的顶部(0,0)到(window.innerWidth, window.innerHeight)的可视区域。

要弄清楚哪些东西需要显示在屏幕上,这个涉及到渲染树的递归遍历。

Repaint(重绘) and Reflow(回流)

通常(页面)都需要至少一次layout(回流)和一次重绘(除非,你的页面是空的:))。之后,改变渲染树都会导致以下一种或两种事情发生:

  1. 需要重新验证渲染树(部分或整个)以及重新计算节点尺寸。这就是回流。注意一定会有一次回流发生-页面的初始布局
  2. 页面的部分需要更新,一个节点的几何属性发生变化,又或是样式的变化(比如更改了background color),这个更新就叫做重绘。

重绘 和 回流 消耗比较大,他们会降低用户体验以及卡UI。

Repaint(重绘)

顾名思义,重绘只不过是屏幕上重新绘制元素,因为元素更改的外观会影响元素的可见性,但不会影响布局。 比如下面几个将会触发重绘:

  1. 更改元素的可见性(visibility,opacity应该也算 )
  2. 更改元素的边框
  3. 更改元素的背景

Reflow(回流)

回流是重新计算文档中元素的位置和几何形状,以便重新显示文档的部分或全部。因为回流会阻止用户操作,所以开发者有必要知道怎样优化回流时间,了解各种文档属性(DOM深度,CSS效率,不同类型的样式更改)对回流时间的影响。有时候回流单个元素可能会导致它的父元素们以及它其后的所有元素的回流。

重绘与回流no-no篇

虚拟DOM和真实DOM

每次DOM有改变,浏览器需要重新计算CSS,回流及重绘页面。这就是为啥真实DOM比较耗时。

为了最小化这个耗时,Ember 使用了 key/value 观察者技术,Angular使用了脏检测。用这种技术,就能够只更新变化了的dom节点(在Angular里被标记为脏的节点)。

不过,现代浏览器也变得聪明了,尝试缩短重绘屏幕所需的时间。最大的变化就是能够重绘批处理DOM的更改。

React虚拟dom背后的理念就是减少和缓存dom变化

为啥React的虚拟Dom很快?

React没有做什么新的事情,只是在策略上有动作。它把真实DOM存一份副本到内存里。当你修改dom了,他先把(这个修改)应用到内存里的DOM,然后使用它的对比算法,找出哪个是真的有变化。

最后,它(react)批量处理这些变化并且一次性应用到真实的DOM上。所以,最小化了回流和重绘。

原文