概述
不同的浏览器内核不同,所以渲染过程不太一样。
(图:WebKit 主流程)
(图:Mozilla 的 Gecko 呈现引擎主流程)
由上面两张图可以看出,虽然主流浏览器渲染过程叫法有区别,但是主要流程还是相同的:
Gecko 将视觉格式化元素组成的树称为“框架树”。每个元素都是一个框架。WebKit 使用的术语是“呈现树”,它由“呈现对象”组成。对于元素的放置,WebKit 使用的术语是“布局”,而 Gecko 称之为“重排”。对于连接 DOM 节点和可视化信息从而创建呈现树的过程,WebKit 使用的术语是“附加”。
所以,浏览器的渲染过程大体如下:
- HTML解析出DOM Tree
- CSS解析出Style Rules
- 将二者关联生成Render Tree
- Layout 根据Render Tree计算每个节点的信息
- 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差不多就是
HTML Parser将文本的HTML文档,提炼出关键信息,嵌套层级的树形结构,便于计算拓展。
CSS解析
CSS Parser将CSS解析成Style Rules,Style Rules也叫CSSOM(CSS Object Model)。Style Rules也是一个树形结构,根据CSS文件整理出来的类似DOM Tree的树形结构:
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加载
- Js加载时会阻塞后续DOM解析
- Js加载时会阻塞页面渲染
- chrome中Js加载会阻塞其他资源(如 CSS,Js 或图片资源)的加载
这个很好理解:
- Js运行在浏览器中,是单线程的,每个 window 一个 Js 线程,所以当然会阻塞后续Js加载。
- Js有可能会修改 DOM 结构,给 DOM 添加样式等等,所以这就意味着在当前 Js 加载执行完成前,后续资源的加载可能是没有意义的。
css加载
- 样式表在下载完成后,将和以前下载的所有样式表一起进行解析,解析完成后,将对此前所有元素(含以前已经渲染的)重新进行渲染。
- css会阻塞页面渲染
- css会阻塞js文件加载
- css不会阻塞DOM解析
- css不会阻塞图片加载。
img加载
图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码。
iframe加载
iframe是因为它可以和主页面并行加载,不会阻塞主页面。
- iframe会阻塞主页面的onload事件,可以关注这里
- 主页面和iframe共享同一个连接池
加快HTML页面加载的方法
- 页面减肥,可以删除不必要的空格、注释,内部的script。
- 减少文件数量,减少HTTP连接数,可以合并的js和css文件尽量合并,图片可以用CSS Sprite技术合并拼接。
- 优化页面元素加载顺序,与页面最初展示相关的js和css放在页面前面使其优先加载,与之无关的放到最后使其最后加载。
- 指定图片和tables的大小。如果浏览器渲染的时候知道了图片或tables的大小,那么它可以给图片和tables做好布局,而不是图片拿到后回退重绘布局。
- 减少域名查询。DNS查询和解析域名也是消耗时间的,所以尽量减少对外部JS、CSS、图片等资源的引用。
- 使用Web Worker(单独的子线程,不影响js主线程)来处理密集型的js计算。
- 这里再提下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”方法。
样式计算
DOM中的一个元素可以对应样式表中的多个元素。样式表包括了所有样式:浏览器默认样式表,自定义样式表,inline样式元素,HTML可视化属性如:width=100(将转化以匹配CSS样式)。
WebKit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系,并且:
- 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是“:hover”状态,而另一个不是)
- 任何元素都没有 ID
- 标记名称应匹配
- 类属性应匹配
- 映射属性的集合必须是完全相同的
- 链接状态必须匹配
- 焦点状态必须匹配
- 任何元素都不应受属性选择器的影响,这里所说的“影响”是指在选择器中的任何位置有任何使用了属性选择器的选择器匹配
- 元素中不能有任何 inline 样式属性
- 不能使用任何同级选择器。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是这个样子:
CSS Rule Tree会是这个样子:
通过这两个树,我们可以得到一个叫Style Context Tree,也就是下面这样(把CSS Rule结点Attach到DOM 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文档根元素,然后下一级渲染对象,如对应着元素,如此层层递归,依次计算每一个渲染对象的几何信息(位置和尺寸)。
-
回流(reflow)
如果某元素的几何尺寸发生了变化,需要重新布局,称其为回流(reflow),本质上仍是布局。reflow 会从
<html>
这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置。以下操作有可能导致reflow:
- 增加、删除、或改变 DOM 节点
- 增加、删除 ‘class’ 属性值
- 元素尺寸改变
- 文本内容改变
- 浏览器窗口改变大小或拖动
- 动画效果进行计算和改变 CSS 属性值
- 伪类激活(:hover)
-
脏位系统
DOM Tree里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow,造成高额的渲染成本。
大多数web应用对DOM的操作都是比较频繁,这意味着经常需要对DOM进行布局和回流,而如果仅仅是一些小改变,就触发整个渲染树的回流,这显然是不好的。为了避免这种情况,浏览器使用了脏位系统,只有一个渲染对象改变了或者某渲染对象及其子渲染对象脏位值为”dirty”时,说明需要回流。
表示需要布局的脏位值有两种:
- “dirty”–自身改变,需要回流
- “children are dirty”–子节点改变,需要回流
-
异步reflow
我们的浏览器是不会每改一次样式,它就 reflow 一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次 reflow ,这又叫异步 reflow 或增量异步 reflow 。但是有些情况浏览器是不会这么做的,比如:Resize 窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行 reflow 。
-
布局的顺序
每一个渲染对象的布局流程基本如下:
-
1.计算此渲染对象的宽度(width);
-
2.遍历此渲染对象的所有子级,依次:
-
2.1设置子级渲染对象的坐标
-
2.2判断是否需要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height)
-
-
3.设置此渲染对象的高度:根据子渲染对象的累积高,margin和padding的高度设置其高度;
-
4.设置此渲染对象脏位值。
-
绘制(Painting)
布局完成后,下一步会进行绘制(Painting)。在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,使用Native GUI等用户界面基础组件,将呈现器的内容显示在屏幕上。
-
绘制的顺序
CSS2 规范定义了绘制流程的顺序,其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。
块呈现器的堆栈顺序如下:
- 背景颜色
- 背景图片
- 边框
- 子代
- 轮廓
-
repaint
屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变(和reflow最大的区别)。