浏览器的执行(2)-浏览器渲染流程

1,079 阅读11分钟

概述

不同的浏览器内核不同,所以渲染过程不太一样。

 clipboard.png

(图:WebKit 主流程)

clipboard.png

(图:Mozilla 的 Gecko 呈现引擎主流程)

由上面两张图可以看出,虽然主流浏览器渲染过程叫法有区别,但是主要流程还是相同的:

Gecko 将视觉格式化元素组成的树称为“框架树”。每个元素都是一个框架。WebKit 使用的术语是“呈现树”,它由“呈现对象”组成。对于元素的放置,WebKit 使用的术语是“布局”,而 Gecko 称之为“重排”。对于连接 DOM 节点和可视化信息从而创建呈现树的过程,WebKit 使用的术语是“附加”。

所以,浏览器的渲染过程大体如下:

  1. HTML解析出DOM Tree
  2. CSS解析出Style Rules
  3. 将二者关联生成Render Tree
  4. Layout 根据Render Tree计算每个节点的信息
  5. Painting 根据计算好的信息绘制整个页面

而针对流程划分,可视为2部分:解析和渲染

解析

DOM解析

HTML Parser的任务是将HTML标记解析成DOM Tree,可以参考《How browsers work》

举个例子,有一个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>

经过解析之后的DOM Tree差不多就是

DOM Tree

HTML Parser将文本的HTML文档,提炼出关键信息,嵌套层级的树形结构,便于计算拓展。

CSS解析

CSS Parser将CSS解析成Style Rules,Style Rules也叫CSSOM(CSS Object Model)。Style Rules也是一个树形结构,根据CSS文件整理出来的类似DOM Tree的树形结构:

Style Rules

CSS Parser将很多个CSS文件中的样式合并,比对着Dom Tree结构,从左到右解析出具有树形结构Style Rules。

脚本解析

浏览器解析文档,当遇到<script>标签的时候,会立即解析脚本,停止解析文档。因为JS可能会改动DOM和CSS,所以继续解析会造成浪费。 如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。

可以在script标签上增加属性defer或者async,用于异步加载脚本文件。

async,不考虑依赖关系,只要下载完后就加载,不考虑此时页面样式先后的加载顺序,不过它对于那些可以> 不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的。

耗时较长的脚本代码可以使用 defer 来推迟执行。

现代浏览器有prefetch功能,浏览器在获得html文档之后会对页面上引用的资源进行提前下载,所以 defer, async 可能并没有太多的用途。

脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM Tree和Style Rules上。

解析加载顺序

html页面在解析DOM的过程中,会遇见img、js、css等文件,此时浏览器会单独下载、并行加载该文件。具体逻辑可以参考这里

Js加载
  1. Js加载时会阻塞后续DOM解析
  2. Js加载时会阻塞页面渲染
  3. chrome中Js加载会阻塞其他资源(如 CSS,Js 或图片资源)的加载

这个很好理解:

  1. Js运行在浏览器中,是单线程的,每个 window 一个 Js 线程,所以当然会阻塞后续Js加载。
  2. Js有可能会修改 DOM 结构,给 DOM 添加样式等等,所以这就意味着在当前 Js 加载执行完成前,后续资源的加载可能是没有意义的。
css加载
  1. 样式表在下载完成后,将和以前下载的所有样式表一起进行解析,解析完成后,将对此前所有元素(含以前已经渲染的)重新进行渲染。
  2. css会阻塞页面渲染
  3. css会阻塞js文件加载
  4. css不会阻塞DOM解析
  5. css不会阻塞图片加载。
img加载

图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码。

iframe加载

iframe是因为它可以和主页面并行加载,不会阻塞主页面。

  • iframe会阻塞主页面的onload事件,可以关注这里
  • 主页面和iframe共享同一个连接池
加快HTML页面加载的方法
  1. 页面减肥,可以删除不必要的空格、注释,内部的script。
  2. 减少文件数量,减少HTTP连接数,可以合并的js和css文件尽量合并,图片可以用CSS Sprite技术合并拼接。
  3. 优化页面元素加载顺序,与页面最初展示相关的js和css放在页面前面使其优先加载,与之无关的放到最后使其最后加载。
  4. 指定图片和tables的大小。如果浏览器渲染的时候知道了图片或tables的大小,那么它可以给图片和tables做好布局,而不是图片拿到后回退重绘布局。
  5. 减少域名查询。DNS查询和解析域名也是消耗时间的,所以尽量减少对外部JS、CSS、图片等资源的引用。
  6. 使用Web Worker(单独的子线程,不影响js主线程)来处理密集型的js计算。
  7. 这里再提下SharedWorker:
    • SharedWorker由独立的进程管理。
    • SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用。
    • Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。

渲染

呈现树(Render Tree)

Render Tree的构建其实就是DOM Tree和Style Rules进行附加(Attach)的过程。所以,Render Tree实际上就是一个计算好样式,与HTML对应的(包括哪些显示,那些不显示)的Tree。

在 WebKit 中,解析样式和创建呈现器的过程称为“附加”。每个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。

Render Tree

样式计算

DOM中的一个元素可以对应样式表中的多个元素。样式表包括了所有样式:浏览器默认样式表,自定义样式表,inline样式元素,HTML可视化属性如:width=100(将转化以匹配CSS样式)。

WebKit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系,并且:

  1. 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是“:hover”状态,而另一个不是)
  2. 任何元素都没有 ID
  3. 标记名称应匹配
  4. 类属性应匹配
  5. 映射属性的集合必须是完全相同的
  6. 链接状态必须匹配
  7. 焦点状态必须匹配
  8. 任何元素都不应受属性选择器的影响,这里所说的“影响”是指在选择器中的任何位置有任何使用了属性选择器的选择器匹配
  9. 元素中不能有任何 inline 样式属性
  10. 不能使用任何同级选择器。WebCore 在遇到任何同级选择器时,只会引发一个全局开关,并停用整个文档的样式共享(如果存在)。这包括 + 选择器以及 :first-child 和 :last-child 等选择器。

在Firefox中,还采用了另外两种树:规则树和样式上下文树。假设我们有下面的HTML文档:

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

对应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; }

于是DOM Tree是这个样子:

DOM Tree

CSS Rule Tree会是这个样子:

CSS Rule Tree

通过这两个树,我们可以得到一个叫Style Context Tree,也就是下面这样(把CSS Rule结点Attach到DOM Tree上):

Style Context Tree

所以,Firefox基本上来说是通过CSS 解析 生成 CSS Rule Tree,然后,通过比对DOM生成Style Context Tree,然后Firefox通过把Style Context Tree和其Render Tree(Frame Tree)关联上,就完成了。

注意:Render Tree会把一些不可见的结点去除掉。

以正确的层叠顺序应用规则

如果某个属性未由任何匹配规则所定义,那么部分属性就可由父代元素样式对象继承。其他属性具有默认值。如果定义不止一个,需要通过层叠顺序来解决(即样式优先级)。

从CSS代码存放位置看权重优先级:内嵌样式 > 内部样式表 > 外联样式表。

从样式选择器看权重优先级:important > 内嵌样式 > ID > 类 > 标签 | 伪类 | 属性选择 > 伪对象 > 继承 > 通配符。

实际比较时,可按照选择器的权重之和比较,和越大优先级越高:

  • 内联属性的权重为1,0,0,0 -- style=''
  • important的权重为1,0,0,0 -- !important
  • ID的权重为0,1,0,0 -- #id
  • 类的权重为0,0,1,0 --.level
  • 伪类的权重为0,0,1,0 -- :active
  • 属性的权重为0,0,1,0 -- [rel=up]
  • 标签的权重为0,0,0,1 -- span
  • 伪对象的权重为0,0,0,1 -- :first-line
  • 通配符的权重为0,0,0,0 -- *
布局(Layout)

创建好呈现树后,下一步会进行布局(Layout)。在这个过程中,根据呈现树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置。

布局是一个从上到下,从外到内进行的递归过程,从根渲染对象,即对应着HTML文档根元素,然后下一级渲染对象,如对应着元素,如此层层递归,依次计算每一个渲染对象的几何信息(位置和尺寸)。

  1. 回流(reflow)

    如果某元素的几何尺寸发生了变化,需要重新布局,称其为回流(reflow),本质上仍是布局。reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置。

    以下操作有可能导致reflow:

    • 增加、删除、或改变 DOM 节点
    • 增加、删除 ‘class’ 属性值
    • 元素尺寸改变
    • 文本内容改变
    • 浏览器窗口改变大小或拖动
    • 动画效果进行计算和改变 CSS 属性值
    • 伪类激活(:hover)
  2. 脏位系统

    DOM Tree里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow,造成高额的渲染成本。

    大多数web应用对DOM的操作都是比较频繁,这意味着经常需要对DOM进行布局和回流,而如果仅仅是一些小改变,就触发整个渲染树的回流,这显然是不好的。为了避免这种情况,浏览器使用了脏位系统,只有一个渲染对象改变了或者某渲染对象及其子渲染对象脏位值为”dirty”时,说明需要回流。

    表示需要布局的脏位值有两种:

    • “dirty”–自身改变,需要回流
    • “children are dirty”–子节点改变,需要回流
  3. 异步reflow

    我们的浏览器是不会每改一次样式,它就 reflow 一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次 reflow ,这又叫异步 reflow 或增量异步 reflow 。但是有些情况浏览器是不会这么做的,比如:Resize 窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行 reflow 。

  4. 布局的顺序

    每一个渲染对象的布局流程基本如下:

    • 1.计算此渲染对象的宽度(width);

    • 2.遍历此渲染对象的所有子级,依次:

      • 2.1设置子级渲染对象的坐标

      • 2.2判断是否需要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height)

    • 3.设置此渲染对象的高度:根据子渲染对象的累积高,margin和padding的高度设置其高度;

    • 4.设置此渲染对象脏位值。

绘制(Painting)

布局完成后,下一步会进行绘制(Painting)。在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,使用Native GUI等用户界面基础组件,将呈现器的内容显示在屏幕上。

  1. 绘制的顺序

    CSS2 规范定义了绘制流程的顺序,其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。

    块呈现器的堆栈顺序如下:

    1. 背景颜色
    2. 背景图片
    3. 边框
    4. 子代
    5. 轮廓
  2. repaint

    屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变(和reflow最大的区别)。

参考链接

  1. 《How Browser Work》
  2. 浏览器渲染页面过程与页面优化
  3. 浏览器的渲染原理简介