一、认识浏览器内核
Trident(IE)
Gecko(火狐)
Blink(Chrome、Opera)
Webkit(Safari)
二、页面如何加载
- 浏览器根据DNS 服务器得到域名的IP 地址
- 向这个 IP 的机器发送HTTP请求
- 服务器收到、处理并返回 HTTP 请求(HMTL 格式的字符串)
- [图片上传中...(浏览器的渲染机制.png-43fc77-1575345132986-0)]
浏览器得到返回内容(其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求)
例如:在浏览器输入https://juejin.im,然后经过 DNS 解析,juejin.im对应的 IP 是36.248.217.149(不同时间、地点对应的 IP 可能会不同)。然后浏览器向该 IP 发送 HTTP 请求。
服务端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容如下:
三、浏览器渲染过程
1. 浏览器解析三个东西
- 一是HTML/SVG/XHTML,HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。
- 二是CSS,解析CSS会产生CSS规则树,它和DOM结构比较像。
- 三是Javascript脚本,等到Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM树和 CSS规则树。
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绘制
1)构建DOM 树:浏览器会遵守一套步骤将HTML 文件转换为 DOM 树(字节数据 => 字符串 => Token => Node => DOM)
浏览器从磁盘或网络读取HTML的原始字节(0和1),并根据文件的指定编码(例如 UTF-8)将它们转换成字符串(我们写的代码)
将字符串转换成Token,例如:、等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。
生成节点对象并构建DOM事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会马上创建节点对象。
2)构建CSSOM:构建CSSOM的过程与构建DOM的过程非常相似(字节数据 => 字符串 => Token => Node => CSSOM)
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。
注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。
3)构建渲染树
当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。
4. 布局与绘制
- 当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
- 布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
- 布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。
注意:回流和重绘:1.计算CSS样式 2.构建Render Tree 3.Layout – 定位坐标和大小 4.正式开画
提问1:浏览器如果渲染过程中遇到JS文件怎么处理?
渲染过程中,如果遇到 < script > 就停止渲染,执行 JS 代码。因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。
提问2: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。
3.async和defer的作用是什么?有什么区别?
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
<script src="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script async src="script.js"></script> (异步下载)
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
<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.你真的了解回流和重绘吗?
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优化: 标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能