浏览器的渲染原理

浏览器工作大致流程

先上一张图:

浏览器渲染大致流程

从图中我们可以知道:

  1. 浏览器会解析三个东西:
    1. 一个是 HTML/SVG/XHTML,事实上,webkit 有三个 C++ 的类对应这三类文档。解析这三种文件产生一个 DOM Tree。
    2. CSS,解析 CSS 会产生 CSS 规则树
    3. JavaScript,脚本,主要是通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree
  2. 解析完成后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree,注意:
    1. Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树上
    2. CSS 的 Rule Tree 主要是为了完成匹配并把 CSS Rule 附加上 Rendering Tree 上的每个 Element。也就是 DOM 节点,也就是所谓的 Frame。
    3. 然后,计算每个 Frame(也就是每个 Element)的位置,这又叫 layout 和 reflow(回流)过程
  3. 最后通过调用操作系统 Native GUI 的 API 绘制

DOM 解析

HTML 的 DOM Tree 解析如下:

<html>
    <html>
        <head>
            <title>Web page parsing</title>
        </head>
        <body>
            <div>
                <h1>Web page parsing</h1>
                <p>This is an example Web page.</p>
            </div>
        </body>
    </html>
</html>

上面这段 HTML 会解析成这样:

DOM-Tree-01

下面是另一个有 SVG 标签的情况:

DOM-Tree-02

CSS 解析

CSS 的解析大概是下面这个样子(下面是 Firefox 的玩法),假设我们有下面的 HTML 文档:

<doc>
    <title>A few quotes</title>
    <para class="emph">
        Franklin said that
        <quote>"A penny saved is a penny earned."</quote>
    </para>
    <para>
        FDR said
        <quote>
            "We have nothing to fear but
            <span class="emph">fear itself.</span>
            "
        </quote>
    </para>
</doc>

于是 DOM Tree 是这个样子:

DOM-Tree-Example

然后我们的 CSS 文档是这样的:

/* rule 1 */
doc {
    display: block;
    text-indent: 1em;
}
/* rule 2 */
title {
    display: block;
    font-size: 3em;
}
/* rule 3 */
para {
    display: block;
}
/* rule 4 */
[class='emph'] {
    font-style: italic;
}

于是我们的 CSS Rule Tree 会是这个样子:

CSS-Rule-Tree-Example

途中的第四条规则出现了两次,一次是独立的,一次是在规则 3 的子节点。所以,我们可以知道,建立 CSS Rule Tree 是需要比照着 DOM Tree 来着。CSS 匹配 DOM Tree 主要是从右到左解析 CSS 的 Selector,好多人以为这个事会比较快,其实并不一定。关键还看我们的 CSS 的 Selector 怎么写了。

注意:CSS 匹配 HTML 元素是一个相对复杂和有性能问题的事情。所以,你就会在 N 多地方看到很多人都告诉你,DOM 树要小,CSS 尽量用 id 和 class ,千万不要过度层叠下去......

所以,Firefox 基本上来说是通过 CSS 解析生成 CSS Rule Tree,然后,通过对比 DOM 生成 Style Context Tree,然后 Firefox 通过把 Style Content Tree 和其 Render Tree(Frame Tree)关联上,就完成了。主要:Render Tree 会把一些不可见的节点去除掉。而 Firefox 中所谓的 Frame 就是一个 DOM 节点,不要被其名字所迷惑了

Firefox-style-context-tree

注:Webkit 不像 Firefox 要用两个树来干这个,Webkit 也有 Style 对象,它直接把这个 Style 对象存在了相应的 DOM 结点上了

渲染

渲染的流程基本上如下(黄色的四个步骤):

  1. 计算 CSS 样式
  2. 构建 Render Tree
  3. Layout - 定位坐标和大小,是否换行,各种 position,overflow,z-index 属性...
  4. 正式绘制

Firefox-style-context-tree

注意:上图流程中有很多连接线,这表示了 JavaScript 动态修改了 DOM 属性或时 CSS 属性会导致重新 Layout,有些改变不会,就是那些指到天上的箭头,比如,修改后的 CSS rule 没有被匹配到等

这里重要说两个概念,一个是 Repaint ,另一个是 Reflow,这两个不是一回事

  • Repaint——屏幕的一部分要重画,比如某个 CSS 的背景色变了。但是元素的集合尺寸没有变

  • Reflow——意味着元件的几何尺寸变了,我们需要重新验证并计算 Render Tree。是 Render Tree 的一部分或全部发生了变化。这就是 Reflow,或是 Layout(HTML 使用的是 flow based layout,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,需要重更新布局,也就叫 Reflow)。Reflow 会从<html> 这个 root frame 开始递归往下,一次计算所有的结点几何尺寸和位置,在 Reflow 过程中,可能会增加一些 frame,比如一个文本字符串必须被包装起来

总之,Reflow 的成本比 Repaint 的成本高得多。DOM Tree 里的每个节点都会有 Reflow 方法,一个节点的 Reflow 很有可能导致子节点,甚至父点以及同级节点的 Reflow

所以,以下动作很大可能会是成本比较高的:

  • 当你增加、删除、修改 DOM 节点时,会导致 Reflow 或 Repaint
  • 当你移动 DOM 的位置,或时搞个动画的时候
  • 当你修改 CSS 样式的时候
  • 当你 Resize 窗口的时候(移动端没有这个问题),或是滚动的时候
  • 当你修改网页的默认字体时

注:display:none 会触发 Reflow,而 visibility: hidden 只会触发 Repaint,因为没有发现位置变化

我们的浏览器时聪明的,它会把修改的操作积攒一批,然后做一次 Reflow,这叫做异步 Reflow 或增量异步 Reflow。但有些情况浏览器不会这样做,比如:resize 窗口、改变了页面某人的字体等。对于这些操作,浏览器会马上进行 Reflow

减少 reflow(回流)/repaint(重绘)

下面时一些 Best Practices:

1) 不要一条一条地修改 DOM 的样式。与其这样,不如预先定义好 CSS 的 class,然后修改 DOM 的 className

// bad
var left = 10,
    top = 10;
el.style.left = left + 'px';
el.style.top = top + 'px';

// Good
el.className += 'theclassname';

// Good
el.style.cssText += ';left:' + left + 'px;top:' + top + 'px;';

2) 把 DOM "离线"后修改。如:

  • 使用 documentFragment 对象在内存里操作 DOM
  • 先把 DOM 给 display:none(有一次 Reflow),然后你想怎么改就怎么改。比如修改 100 次,然后再把它显示出来
  • clone 一个 DOM 节点到内存里,然后想怎么改就怎么改,改完后,和在线的那个交换一下

**3)不要把 DOM 节点的属性值放在一个循环里当作循环的变量。**不然这会导致大量地读写这个节点的属性

4)尽可能地修改层级比较低的 DOM。当然,改变层级比较底的 DOM 也有可能造成大面积的 Reflow,但是也可能影响范围很小

5)为动画的 HTML 元件使用 fixed 或 absolute 的 position,那么修改它们的 CSS 是不会 Reflow 的

**6)千万不要使用 table 布局。**因为可能很小的一个改动会造成整个 table 的重新布局

总结

浏览器渲染三个步骤,解析,渲染,绘制

解析:三种语言都要分析,HTML 被解析为 DOM 树,CSS 被解析成 CSS 规则树,JavaScript 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree

渲染:浏览器引擎通过 DOM Tree 和 CSS Rule Tree 构建 Rendering Tree(渲染树),这其中进行大量的 Reflow 和 Repaint

绘制:最后调用操作系统的 Native GUI 的 API 绘制

参考资料

Last Updated:
Contributors: johan