你不知道的浏览器页面渲染机制

前言

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另外一个是JS引擎。渲染引擎在不一样的浏览器中也不是都相同的。目前市面上常见的浏览器内核能够分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面你们最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。本文咱们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。html

想阅读更多优质文章请猛戳GitHub博客,一年五十篇优质文章等着你!前端

页面加载过程

在介绍浏览器渲染过程以前,咱们简明扼要介绍下页面的加载过程,有助于更好理解后续渲染过程。git

要点以下:github

  • 浏览器根据 DNS 服务器获得域名的 IP 地址
  • 向这个 IP 的机器发送 HTTP 请求
  • 服务器收到、处理并返回 HTTP 请求
  • 浏览器获得返回内容

例如在浏览器输入https://juejin.im/timeline,而后通过 DNS 解析,juejin.im对应的 IP 是36.248.217.149(不一样时间、地点对应的 IP 可能会不一样)。而后浏览器向该 IP 发送 HTTP 请求。面试

服务端接收到 HTTP 请求,而后通过计算(向不一样的用户推送不一样的内容),返回 HTTP 请求,返回的内容以下:shell

其实就是一堆 HMTL 格式的字符串,由于只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。segmentfault

浏览器渲染过程

浏览器渲染过程大致分为以下三部分:浏览器

1)浏览器会解析三个东西:

  • 一是HTML/SVG/XHTML,HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。

  • 二是CSS,解析CSS会产生CSS规则树,它和DOM结构比较像。

  • 三是Javascript脚本,等到Javascript 脚本文件加载后, 经过 DOM API 和 CSSOM API 来操做 DOM Tree 和 CSS Rule Tree。

2)解析完成后,浏览器引擎会经过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。

  • Rendering Tree 渲染树并不等同于DOM树,渲染树只会包括须要显示的节点和这些节点的样式信息。
  • CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每一个Element(也就是每一个Frame)。
  • 而后,计算每一个Frame 的位置,这又叫layout和reflow过程。

3)最后经过调用操做系统Native GUI的API绘制。

接下来咱们针对这其中所经历的重要步骤详细阐述

构建DOM

浏览器会遵照一套步骤将HTML 文件转换为 DOM 树。宏观上,能够分为几个步骤:性能优化

构建DOM的具体步骤

  • 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据之后,它会将这些字节数据转换为字符串,也就是咱们写的代码。服务器

  • 将字符串转换成Token,例如:<html><body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息

这时候你必定会有疑问,节点与节点之间的关系如何维护?

事实上,这就是Token要标识“起始标签”和“结束标签”等标识的做用。例如“title”Token的起始标签和结束标签之间的节点确定是属于“head”的子节点。

上图给出了节点之间的关系,例如:“Hello”Token位于“title”开始标签与“title”结束标签之间,代表“Hello”Token是“title”Token的子节点。同理“title”Token是“head”Token的子节点。

  • 生成节点对象并构建DOM

事实上,构建DOM的过程当中,不是等全部Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每一个Token被生成后,会马上消耗这个Token建立出节点对象。注意:带有结束标签标识的Token不会建立节点对象。

接下来咱们举个例子,假设有段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会解析成这样:

构建CSSOM

DOM会捕获页面的内容,但浏览器还须要知道页面如何展现,因此须要构建CSSOM。

构建CSSOM的过程与构建DOM的过程很是类似,当浏览器接收到一段CSS,浏览器首先要作的是识别出Token,而后构建节点并生成CSSOM。

在这一过程当中,浏览器会肯定下每个节点的样式究竟是什么,而且这一过程实际上是很消耗资源的。由于样式你能够自行设置给某个节点,也能够经过继承得到。在这一过程当中,浏览器得递归 CSSOM 树,而后肯定具体的元素究竟是什么样式。

注意:CSS匹配HTML元素是一个至关复杂和有性能问题的事情。因此,DOM树要小,CSS尽可能用id和class,千万不要过渡层叠下去

构建渲染树

当咱们生成 DOM 树和 CSSOM 树之后,就须要将这两棵树组合为渲染树。

在这一过程当中,不是简单的将二者合并就好了。渲染树只会包括须要显示的节点和这些节点的样式信息,若是某个节点是 display: none 的,那么就不会在渲染树中显示。

咱们或许有个疑惑:浏览器若是渲染过程当中遇到JS文件怎么处理

渲染过程当中,若是遇到<script>就中止渲染,执行 JS 代码。由于浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。

也就是说,若是你想首屏渲染的越快,就越不该该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的缘由。固然在当下,并非说 script 标签必须放在底部,由于你能够给 script 标签添加 defer 或者 async 属性(下文会介绍这二者的区别)。

JS文件不仅是阻塞DOM的构建,它会致使CSSOM也阻塞DOM的构建

本来DOM和CSSOM的构建是互不影响,井水不犯河水,可是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。

这是什么状况?

这是由于JavaScript不仅是能够改DOM,它还能够更改样式,也就是它能够更改CSSOM。由于不完整的CSSOM是没法使用的,若是JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必需要能拿到完整的CSSOM。因此就致使了一个现象,若是浏览器还没有完成CSSOM的下载和构建,而咱们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种状况下,浏览器会先下载和构建CSSOM,而后再执行JavaScript,最后在继续构建DOM

布局与绘制

当浏览器生成渲染树之后,就会根据渲染树来进行布局(也能够叫作回流)。这一阶段浏览器要作的事情是要弄清楚各个节点在页面中的确切位置和大小。一般这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每一个元素在视口内的确切位置和尺寸,全部相对测量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会当即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

以上咱们详细介绍了浏览器工做流程中的重要步骤,接下来咱们讨论几个相关的问题:

几点补充说明

1.async和defer的做用是什么?有什么区别?

接下来咱们对比下 defer 和 async 属性的区别:

async和defer

其中蓝色线表明JavaScript加载;红色线表明JavaScript执行;绿色线表明 HTML 解析。

1)状况1<script src="script.js"></script>

没有 defer 或 async,浏览器会当即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

2)状况2<script async src="script.js"></script> (异步下载)

async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,若是已经加载好,就会开始执行——不管此刻是 HTML 解析阶段仍是 DOMContentLoaded 触发以后。须要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发以前或以后执行,但必定在 load 触发以前执行。

3)状况3 <script defer src="script.js"></script>(延迟执行)

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未中止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成以后(这两件事情的顺序无关),会执行全部由 defer-script 加载的 JavaScript 代码,而后触发 DOMContentLoaded 事件。

defer 与相比普通 script,有两点区别:**载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成以后。
在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。**

2.为何操做 DOM 慢

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁链接。——《高性能 JavaScript》

JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操做并不是 JS 一我的的独舞,而是两个模块之间的协做。

由于 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当咱们用 JS 去操做 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口做为“桥梁”(以下图)。

过“桥”要收费——这个开销自己就是不可忽略的。咱们每操做一次 DOM(无论是为了修改仍是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。所以“减小 DOM 操做”的建议,并不是空穴来风。

3.你真的了解回流和重绘吗

渲染的流程基本上是这样(以下图黄色的四个步骤):1.计算CSS样式 2.构建Render Tree 3.Layout – 定位坐标和大小 4.正式开画

注意:上图流程中有不少链接线,这表示了Javascript动态修改了DOM属性或是CSS属性会致使从新Layout,但有些改变不会从新Layout,就是上图中那些指到天上的箭头,好比修改后的CSS rule没有被匹配到元素。

这里重要要说两个概念,一个是Reflow,另外一个是Repaint

  • 重绘:当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。
  • 回流:当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来。这个过程就是回流(也叫重排)

咱们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程当中,还会不断从新渲染。从新渲染会重复回流+重绘或者只有重绘。
回流一定会发生重绘,重绘不必定会引起回流。重绘和回流会在咱们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点极可能会致使父节点的一系列回流。

1)常见引发回流属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操做,都会触发回流,

  • 添加或者删除可见的DOM元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容变化,好比用户在input框中输入文字
  • 浏览器窗口尺寸改变——resize事件发生时
  • 计算 offsetWidth 和 offsetHeight 属性
  • 设置 style 属性的值

2)常见引发重绘属性和方法

3)如何减小回流、重绘

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,由于前者只会引发重绘,后者会引起回流(改变了布局)
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会致使回流,由于须要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会形成整个 table 的从新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也能够选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层可以阻止该节点的渲染行为影响别的节点。好比对于 video 标签来讲,浏览器会自动将该节点变为图层。

性能优化策略

基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化能够对页面渲染作些优化,提高页面性能。

  • JS优化: <script> 标签加上 defer属性 和 async属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。

    • defer属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
    • async属性: HTML5新增属性,用于异步下载脚本文件,下载完毕当即解释执行代码。
  • CSS优化: <link> 标签的 rel属性 中的属性值设置为 preload 可以让你在你的HTML页面中能够指明哪些资源是在页面加载完成后即刻须要的,最优的配置加载顺序,提升渲染性能

总结

综上所述,咱们得出这样的结论:

  • 浏览器工做流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
  • CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
  • 一般状况下DOM和CSSOM是并行构建的,可是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂停,若是此时又恰巧浏览器还没有完成CSSOM的下载和构建,因为JavaScript能够修改CSSOM,因此须要等CSSOM构建完毕后再执行JS,最后才从新DOM构建。

给你们推荐一个好用的BUG监控工具Fundebug,欢迎免费试用!

欢迎关注公众号:前端工匠,你的成长咱们一块儿见证!

参考文章