深刻浅出浏览器渲染流程

1、浏览器如何渲染网页

要了解浏览器渲染页面的过程,首先得知道一个名词——关键路径渲染。关键渲染路径(Critical Rendering Path)是指与当前用户操做有关的内容。例如用户在浏览器中打开一个页面,其中页面所显示的东西就是当前用户操做相关的内容,也就是浏览器从服务器那收到的HTML,CSS,JavaScript等相关资源,而后通过一系列处理后渲染出来web页面。实际抽象出来理解能够将这些步骤看做一个函数,就输入HTML,通过一层层的处理,最后输出像素。javascript

而浏览器渲染的过程主要包括如下几步:css

  • 浏览器将获取的HTML文档并解析成DOM树。
  • 将 css 文件处理成 StyleSheet 对象,从而进行样式计算。
  • 根据dom树和StyleSheet 生成布局树。
  • 根据具体的节点信息对页面进行分层处理,生成图层树
  • 根据图层树生成绘制列表
  • 合成线程经过主线程提交的绘制列表对图层进行分块,并进行栅格化,生成位图
  • 合成位图,并将其显示

具体以下图过程以下图所示:
html

渲染流程.PNG

须要注意的是,以上几个步骤并不必定是一次性顺序完成,好比 DOM 被修改时,亦或是哪一个过程会重复执行,这样才能计算出哪些像素须要在屏幕上进行从新渲染。而在实际状况中,JavaScript和CSS的某些操做每每会屡次修改DOM或者CSSOM。html5

值得注意的的是,在每一个阶段,都会有对应的输入,处理,以及输出。下面咱们就来详细的了解一下这几个过程及须要注意的事项。java

2、浏览器渲染网页的具体流程

2.1 构建DOM树

由于浏览器没法直接使用HTML/SVG/XHTML,所以当浏览器客户端从服务器那接受到HTML文档后,就会遍历文档节点,而后对这些文档节点经过HTML解析器进行解析,最后生成DOM树,所生成的 DOM 树结构和HTML标签一一对应。须要注意的是,在这其中HTML解析器会进行诸如:标记化算法,树构建算法等操做,其中的规范即遵循了W3C的相应规范,也都有浏览器引擎本身的一些特定的操做,详情能够翻阅这篇很是著名的文章:node

How Browsers Work: Behind the scenes of modern web browsersweb

在此阶段,输入的便是一个HTML文件,而后会有浏览器的HTML解析器对其进行解析,输出树形结构的DOM树。值得注意的是,HTML解析器并非等整个文档所有加载完以后才开始解析的,而是网络进程加载了多少数据,HTML解析器就会解析多少数据。至关与在网络进程与渲染进程之间会在这期间创建一个数据共享的管道,网络进程每次收到数据都会将其转发到渲染进程,从而保证渲染进程中的HTML解析器能够源源不断的获取到用于渲染的数据。这个过程能够理解为下方这个过程:
算法

  1. 将字节流经过分词器转化为 Token
  2. 根据 Token 生成节点 node
  3. 根据生成的节点,组成 DOM 树

每一个页面的DOM树,咱们也能够直接经过在控制台输入document 来进行访问:
浏览器

企业微信截图_d70fd07c-795f-4117-ba6f-4daa428c5718.png

对于DOM树,咱们须要注意如下几点:服务器

  1. DOM 树从内容上来看和 HTML 几乎如出一辙,但 DOM 是保存在内存中的树形结构,能够经过 JavaScript 来查询和修改。
document.getElementsByTagName("h2")[0].innerText = "Hello World"
复制代码
  1. display:none 的元素也会在 DOM 树中。
  2. 注释也会在 DOM 树中
  3. script 标签会在 DOM 树中
  4. DOM 树在构建的过程当中可能会被 CSS 和 JS 的加载而执行阻塞。

此外DOM 树在构建的过程当中可能会被 CSS 和 JS 的加载而执行阻塞,也就是咱们常说的阻塞渲染。这是由于HTML文件是经过HTML解析器转化成 DOM 树的,而在HTML解析器中若是遇到了 JavaScript 脚本,HTML 解析器会先执行 JavaScript 脚本,待这个脚本执行完成以后,再继续往下解析。所以咱们常说,将script标签放在body下面,一般就是基于这种考虑的。但为何CSS也有可能会阻塞DOM树的构建呢,能够看下面一个栗子:

<html>
    <head> <style type="text/css" src = "demo.css" /> </head> <body> <p>demo</p> <script> const p = document.getElementsByTagName('p')[0] p.innerText = 'hello world' p.style.color = 'red' </script> </body> </html>
复制代码

因为任何script代码都能改变HTML的结构,所以HTML每次遇到script都会中止解析,等待JavaScript脚本被执行完成以后,再进行接下来的解析,而当咱们经过 JavaScript 去进行样式操做的时候,这个 JavaScript 脚本执行完成的前提条件就成了须要现将样式信息肯定下来。所以在这种状况下,HTML解析器可否继续执行下去,以及继续执行的时间,也须要取决与这个CSS文件给不给面子了。这也是咱们常说的,别在 JavaScript 中操做样式的缘由。

为了优化这种状况,现代浏览器也作了一些优化,好比预解析操做。当渲染引擎接收到字节流后,会开启一个预解析线程,用来分析 HTML文件的代码中的JS,CSS文件,解析到相关文件的时候,预解析进行会提早下载这些资源。

对于处理这种事情,避免阻塞的产生,咱们也有如下几点能够注意的:

  • 在引入顺序上,CSS 资源先于 JavaScript 资源。
  • JavaScript 应尽可能少的去影响 DOM 的构建。
  • 能够将 JavaScript 脚本设置为异步加载,经过 async 或 defer 来标记代码

2.2 计算样式

在构建渲染树时,须要计算每个呈现对象的可视化的属性值。而这个过程就被称为样式计算或者计算样式。这个过程主要是为了 DOM 树中每一个节点的具体样式,大体可分为三大步骤:

  1. 将 CSS 解析为浏览器能理解的 StyleSheet
  2. 标准化样式表中的属性值
  3. 计算出 DOM 树中每一个节点的具体样式

2.2.1 将 CSS 解析为浏览器能理解的 styleSheet

和html一个道理,浏览器也没法直接去理解咱们所写的那些CSS样式,所以浏览器在接收到CSS文件后,会将CSS文件转换为浏览器所能理解的 StyleSheet。转化了的 StyleSheet 咱们一样也能够经过控制台来访问:

image.png

在这个过程当中须要注意的是:

  1. CSS解析能够与DOM解析同时进行。
  2. CSS解析与 script 的执行互斥 。
  3. 在Webkit内核中进行了script执行优化,只有在JS访问CSS时才会发生互斥。
  4. CSS样式无论是来自于 link 的外部引用,仍是style标记内的CSS,亦或是元素的style属性内嵌的CSS,都会被解析成styleSheets。

2.2.2 标准化样式表中的属性值

在将CSS文转化为浏览器可以理解的 styleSheet 后,就须要对期进行进行属性值的标准化操做了。这里的标准化的意思就是,咱们在写css文件的时候,会写一些语义化的属性好比:red/bold等等。但其实这些词对于渲染引擎来讲,却不是那么好理解的。所以在进行计算样式以前,浏览器还会这对这些不怎么好计算的值进行标准化,将其转化为渲染引擎容易理解的词,好比将red转化成为 rgb(255, 0, 0)等等。

2.2.3 计算出 DOM 树中每一个节点的具体样式

计算出 DOM 树中每一个节点的具体样式主要涉及的就是CSS继承规则和层叠规则了,对于继承规则其实比较好理解,就是,每一个DOM节点都包含的父节点的样式。

而层叠规则也就是样式层叠就有点麻烦了,MDN是这么描述层叠的:

层叠是CSS的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在CSS处于核心地位,CSS的全称层叠样式表正是强调了这一点。

层叠的具体细节在这里也不展开讲了(我本身如今还没搞清楚。。。),你们能够去CSS层叠看看其内部的一些规则。

在有了css继承规则和层叠规则后,样式计算的这个阶段就会在这两个规则的基础上对 DOM 节点中的每一个元素计算处具体的样式,这个阶段中最终输出的结果会保存在 ComputedStyle 中,这个一样能够经过控制台进行查看:

image.png

**

2.3 布局阶段

经过前面两个阶段,咱们已经获得了DOM树以及DOM树中具体每一个元素的样式了,但对于每一个元素所处的几何位置咱们如今仍是不知道的,所以接下来要作的就是计算出DOM树中可见元素的几何位置。这个过程能够分为两个阶段:

  1. 建立布局树
  2. 布局计算

2.3.1 建立布局树

因为DOM树还包含不少不可见的元素,好比head标签,script标签,以及设置为display:none的属性,由于浏览器势必不能将全部的dom树的元素都所有拿来进行布局计算,所以在这个阶段,浏览器会额外构建一颗只包含可见元素的布局树。在构建布局树期间,浏览器大致会进行如下一些工做:

  • 遍历DOM树中的全部可见节点,并将这些节点加到布局中。
  • 将全部不可见节点忽略掉

下面两个须要注意:

  • display: none的元素不在Render Tree中
  • visibility: hidden的元素在Render Tree中

2.3.2 布局计算

在已经获取了全部可见元素的树以后,就能够计算布局树节点的几何位置了。HTML是基于流的布局方式,所以大多数状况下,只须要进行一次遍历即刻计算出页面的几何信息。一般来讲,处于流靠后的元素不会影响到靠前位置元素的几何特征,所以在进行布局计算的时候,一般是按从左至右,从上至下的顺序遍历文档(只是一般而言,好比表格啥的就不是这样)。

布局计算是一个递归的过程,它从根节点出发,而后递归遍历部分或全部的节点,为每个须要计算的呈现器计算几何信息。这个计算量无疑是庞大的,所以为了不一些较小的更改也会触发页面的总体布局计算,浏览器将布局方式分为了全局布局和增量布局。

  1. 全局布局:全局布局是指触发了整个布局树的布局计算的布局,包括:屏幕大小改动,字体大小改动等
  2. 增量布局:增量布局是指当某个呈现器发生改变了,只对相应的呈现器进行布局计算。

在执行完布局计算后,会将布局计算的结果写入布局树中,所以这个过程能够理解为一种装饰者模式,输入输出都是一个布局树,只是在这个过程当中会将布局计算的结果给加进去。

2.4 分层

在有了布局树以后,浏览器的仍是不能直接根据布局树来将页面给画出来,由于页面中还存在中一些特殊的效果,好比页面滚动,z-index等。为了可以方便的实现这些花里胡哨的功能,渲染引擎还须要进行一个分层处理,将特定节点生成转筒的图层,并生成一个图层树(LayerTree),这个咱们也能经过浏览器的面板看到:

image.png

如上图所示,浏览器的页面实际上被分红了多个图层,这些图层叠加在一块儿就造成了咱们最终所看到的页面。须要注意的是,并非布局树中的每个节点都会包含一个图层,所以若是一个节点没有所对应的图层,那么它就会从属于父节点的图层。若是一个节点须要有本身的图层,一般须要知足如下联合条件

  1. 拥有层叠上下文属性的元素
  2. 须要剪裁(clip)

2.5  图层绘制

在肯定好图层以后,浏览器的渲染引擎会对图层树中的每一个图层进行绘制,渲染引擎会将一个图层的绘制拆封成不少个小的绘制指令,而后会将这些绘制指令按照必定顺序组成一个待绘制列表。和布局相同,绘制也分为全局和增量两种,也是为了不部分图层的改变而须要对整个图层树进行绘制。此外,CSS也对绘制顺序作了规定:

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

2.6  栅格化(raster)操做

这里的栅格化是指将图转化为位图。绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际绘制操做是由渲染引擎中的合成线程来完成的。实际过程是当图层对应的绘制列表准备好以后,主线程会将绘制列表提交给合成线程。 合成线程会根据用户所能见的窗口范围对一些划分,将一些大的图层化分为图块。而后合成线程会根据用户所见范围附近的图块来优先生成位图,实际生成位图的操做是由栅格化来执行的。图块是栅格化执行的最小单元,渲染进程维护了一个栅格化的线程池,全部的图块栅格化操做都会在这个线程池里进行。

一般,栅格化会使用GPU进程中的GPU来进行加速,使用GPU进程生成位图的过程叫快速栅格化,经过这个方式生成的位图会被保存在GPU内存中。这样作的好处就在于,当渲染进程的主线程发生阻塞的时候,合成线程以及GPU进程不会受其影响,能够正常运行。这也是为啥有时候主线程卡住了,但CSS动画依然能够风骚依旧的缘由。

2.7  合成和显示

在全部的图块都被进行栅格化后,合成线程就会生成绘制图块的命令——“DrawQuad”,而后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,而后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

3、浏览器渲染网页的那些事儿

3.1 回流和重绘(reflow和repaint)

咱们都知道HTML默认是流式布局的,但CSS和JS会打破这种布局,改变DOM的外观样式以及大小和位置。所以咱们就须要知道两个概念:

  • reflow(回流):当浏览器发现某个部分发生了变化从而影响了布局,这个时候就须要倒回去从新渲染,你们称这个回退的过程叫 reflow。 常见的reflow是一些会影响页面布局的操做,诸如Tab,隐藏等。reflow 会从 html 这个 root frame 开始递归往下,依次计算全部的结点几何尺寸和位置,以确认是渲染树的一部分发生变化仍是整个渲染树。reflow几乎是没法避免的,由于只要用户进行交互操做,就势必会发生页面的一部分的从新渲染,且一般咱们也没法预估浏览器到底会reflow哪一部分的代码,由于他们会相互影响。
  • repaint(重绘): repaint则是当咱们改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,可是元素的几何尺寸和位置没有发生改变。

须要注意的是,display:none 会触发 reflow,而visibility: hidden属性则并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,它会被渲染成一个空框,这在咱们上面有提到过。因此visibility:hidden 只会触发 repaint,由于没有发生位置变化。

咱们不能避免reflow,但仍是能经过一些操做来减小回流:

  1. 用transform作形变和位移.
  2. 经过绝对位移来脱离当前层叠上下文,造成新的Render Layer。

另外有些状况下,好比修改了元素的样式,浏览器并不会马上reflow 或 repaint 一次,而是会把这样的操做积攒一批,而后作一次 reflow,这又叫异步 reflow 或增量异步 reflow。可是在有些状况下,好比resize 窗口,改变了页面默认的字体等。对于这些操做,浏览器会立刻进行 reflow。

3.2 几条关于优化渲染效率的建议

结合上文和我看到的一些文章,有如下几点能够优化渲染效率

  1. 合法地去书写 HTML 和 CSS ,且不要忘了文档编码类型。
  2. 样式文件应当在 head 标签中,而脚本文件在 body 结束前,这样能够防止阻塞的方式。
  3. 简化并优化CSS选择器,尽可能将嵌套层减小到最小。
  4. 尽可能减小在 JavaScript 中进行DOM操做。
  5. 修改元素样式时,更改其class属性是性能最高的方法。
  6. 尽可能用 transform 来作形变和位移

参考资料:

www.html5rocks.com/en/tutorial…

time.geekbang.org/column/intr…

相关文章
相关标签/搜索