浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另外一个是 JS 引擎。渲染引擎在不一样的浏览器中也不是都相同的。目前市面上常见的浏览器内核能够分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面你们最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。html
本文咱们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。前端
在介绍浏览器渲染过程以前,咱们简明扼要介绍下页面的加载过程,有助于更好理解后续渲染过程。浏览器
要点以下:性能优化
浏览器根据 DNS 服务器获得域名的 IP 地址;服务器
向这个 IP 的机器发送 HTTP 请求;网络
服务器收到、处理并返回 HTTP 请求;架构
浏览器获得返回内容。异步
例如在浏览器输入https://juejin.im/timeline
,而后通过 DNS 解析,juejin.im
对应的 IP 是36.248.217.149
(不一样时间、地点对应的 IP 可能会不一样)。而后浏览器向该 IP 发送 HTTP 请求。async
服务端接收到 HTTP 请求,而后通过计算(向不一样的用户推送不一样的内容),返回 HTTP 请求,返回的内容以下:ide
其实就是一堆 HMTL 格式的字符串,由于只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。
浏览器渲染过程大致分为以下三部分:
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 绘制。
接下来咱们针对这其中所经历的重要步骤详细阐述
浏览器会遵照一套步骤将 HTML 文件转换为 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 的过程当中,不是等全部 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 会解析成这样:
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。
为了帮助你们让学习变得轻松、高效,给你们免费分享一大批资料,帮助你们在成为全栈工程师,乃至架构师的路上披荆斩棘。在这里给你们推荐一个前端全栈学习交流圈:866109386
当浏览器生成渲染树之后,就会根据渲染树来进行布局(也能够叫作回流)。这一阶段浏览器要作的事情是要弄清楚各个节点在页面中的确切位置和大小。一般这一行为也被称为“自动重排”。
布局流程的输出是一个“盒模型”,它会精确地捕获每一个元素在视口内的确切位置和尺寸,全部相对测量值都将转换为屏幕上的绝对像素。
布局完成后,浏览器会当即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。
以上咱们详细介绍了浏览器工做流程中的重要步骤,接下来咱们讨论几个相关的问题:
接下来咱们对比下 defer 和 async 属性的区别:
其中蓝色线表明 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 是有顺序的加载。
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁链接。——《高性能 JavaScript》
JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在 JS 的世界里,一切是简单的、迅速的。但 DOM 操做并不是 JS 一我的的独舞,而是两个模块之间的协做。
由于 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当咱们用 JS 去操做 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口做为“桥梁”(以下图)。
过“桥”要收费——这个开销自己就是不可忽略的。咱们每操做一次 DOM(无论是为了修改仍是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。所以“减小 DOM 操做”的建议,并不是空穴来风。
为了帮助你们让学习变得轻松、高效,给你们免费分享一大批资料,帮助你们在成为全栈工程师,乃至架构师的路上披荆斩棘。在这里给你们推荐一个前端全栈学习交流圈:866109386
渲染的流程基本上是这样(以下图黄色的四个步骤):
1. 计算 CSS 样式
2. 构建 Render Tree
3.Layout – 定位坐标和大小
4. 正式开画
注意:上图流程中有不少链接线,这表示了 Javascript 动态修改了 DOM 属性或是 CSS 属性会致使从新 Layout,但有些改变不会从新 Layout,就是上图中那些指到天上的箭头,好比修改后的 CSS rule 没有被匹配到元素。
这里重要要说两个概念,一个是 Reflow,另外一个是 Repaint
重绘:当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。
回流:当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来,这个过程就是回流(也叫重排)。
咱们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程当中,还会不断从新渲染。从新渲染会重复回流 + 重绘或者只有重绘。
回流一定会发生重绘,重绘不必定会引起回流。重绘和回流会在咱们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点极可能会致使父节点的一系列回流。
任何会改变元素几何信息 (元素的位置和尺寸大小) 的操做,都会触发回流,
添加或者删除可见的 DOM 元素;
元素尺寸改变——边距、填充、边框、宽度和高度;
内容变化,好比用户在 input 框中输入文字;
浏览器窗口尺寸改变——resize 事件发生时;
计算 offsetWidth 和 offsetHeight 属性;
设置 style 属性的值。
使用 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 构建。