1. 浏览器的内核
- Webkit -> Blink: Google Chrome Edge
- Trident(三叉戟):百度、360、IE、搜狗、UC
- Gecko(壁虎):Firefox
- Presto(急板乐曲) -> Blisk: Opera
2. 渲染页面的详细流程
2.1 渲染前的操作
一般,我们输入一个网页的域名,经过 DNS 域名解析,查找到对应的 IP 地址,然后再到对应的服务器上查找 index.html
2.2 渲染过程
- 1) HTML 经过HTML解析器会被转化成对应的 DOM 树(JS 可能会对节点做一些额外的操作);
- 2) 在解析 HTML 的过程中遇到 CSS 样式,那么会由浏览器负责下载对应的 CSS 文件;浏览器下载完 CSS 文件后,就会对 CSS 文件进行解析,解析出对应的规则树,可以称之为 CSSOM(CSS Object Module);
- 下载 CSS 文件是不会影响 DOM 树的解析
- 3) 当有了 DOM Tree 和 CSSOM Tree 后,就可以结合形成渲染树 (Render Tree) 了。
- css link 元素不会阻塞 DOM Tree 的构建过程,但是会阻塞 Render Tree 的构建过程
- 这是因为 Render Tree 在构建时,需要对应的 CSSOM Tree
- Render Tree 和 DOM Tree 并不是一一对应的关系,比如 display:none 的元素,不会出现在 Render Tree 中
- css link 元素不会阻塞 DOM Tree 的构建过程,但是会阻塞 Render Tree 的构建过程
- 4) 布局(Layout)和绘制(Paint)
- 在渲染树上运行布局以计算每个节点的几何体
- 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;
- 布局是确定呈现树中所有节点的宽度、高度和位置信息;
- 将每个节点绘制到屏幕上
- 在绘制阶段,浏览器将布局阶段计算的每个frame转化为屏幕上实际的像素点;
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如 img);
- 在渲染树上运行布局以计算每个节点的几何体
- 回流:reflow,也可以称之为重排。第一次确定节点的大小和位置叫做布局,之后对节点的大小、位置重新计算叫做回流。
回流的条件:
- 比如DOM结构发生改变(添加新的节点或者移除节点);
- 比如改变了布局(修改了width、height、padding、font-size等值);
- 比如窗口resize(修改了窗口的尺寸等);
- 比如调用getComputedStyle方法(上图中 Computed style 重新计算,导致后面操作都会发生改变)获取尺寸、位置信息;
- 重绘:第一次渲染内容称之为绘制(paint);之后重新渲染称之为重绘。
重绘的条件: 修改背景色、文字颜色、边框颜色、样式等。
PS:回流一定会引起重绘,所以说回流是一件很消耗性能的事情。因此在开发过程中尽量避免发生回流:
- 修改样式时尽量一次性修改: 比如通过cssText修改,比如通过添加class修改
- 尽量避免频繁的操作DOM: 我们可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作;
- 尽量避免通过getComputedStyle获取尺寸、位置等信息;
- 对某些元素使用position的absolute或者fixed: 并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响(这些元素已经脱标)。
- 特殊解析-composite合成
- 绘制的过程,可以将布局后的元素绘制到多个合成图层中。这是浏览器的一种优化手段。
- 默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的;
- 而一些特殊的属性,会创建一个新的合成层(CompositingLayer),并且新的图层可以利用 GPU 来加速绘制。因为每个合成层都是单独渲染的;
可以形成新图层的一些属性:
- 3D transforms
- video、canvas、iframe
- opacity 动画转换时
- position: fixed
- will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化
- animation 或 transition 设置了opacity、transform
- 分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用。
3. script 元素和页面解析的关系
3.1 script 在页面中的解析
在浏览器解析HTML的过程中,遇到了script停止构建DOM树,首先下载 JavaScript 代码,并且执行 JavaScript 代码,只有等到 JavaScript 代码执行完成之后,才会继续解析 HTML,构建 DOM 树。
-
原因:
- JavaScript 的作用之一就是操作 DOM,并且修改 DOM;
- 等到DOM树构建完成并且渲染,再执行 JavaScript,会造成严重的回流和重绘,影响页面的性能;
- 所以在遇到 script 元素时,优先下载和执行 JavaScript 代码,再继续构建 DOM 树。
-
此方式会产生新的问题:
- 目前的开发模式中(比如 Vue、React),脚本往往比 HTML 页面更“重”,处理时间更长;
- 会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到;
3.2 解决问题的两个属性(attribute):defer&async
3.2.1 defer
-
defer 属性告诉浏览器不要等待脚本下载,而继续解析 HTML,构建 DOM Tree。
- 脚本由浏览器来进行下载,但不会阻塞DOM Tree 的构建过程;
- 脚本若提前下载好,它会等待 DOM Tree 构建完成,在 DOMContentLoaded 事件之前先执行defer中的代码;
-
所以 DOMContentLoaded 总会等待 defer 中的代码先执行
-
另外多个带defer脚本是可以保持正确的执行顺序的。
-
从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中;
- 注意:defer仅适用于外部脚本,对于script默认内容会被忽略。
3.2.2 async
- async 特性与 defer 类似,它也能够让脚本不阻塞页面。
- async是让一个脚本完全独立执行的:
-
浏览器不会因 async 脚本而阻塞(与 defer 类似);
-
async 脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;
-
async不会保证在 DOMContentLoaded 之前或之后执行;
-
PS:
- defer 通常用于需要在文档解析后操作 DOM 的JavaScript 代码,并且对多个 script 文件有顺序要求;
- async 通常用于独立的脚本,对其他脚本,甚至 DOM 是没有依赖的。
4. V8引擎执行原理
V8 引擎会对 JavaScript 源代码进行解析,形成 抽象语法树(AST),然后经过 Ignition 操作转成字节码,而字节码可以跨平台运行,不过这种方式执行效率很低,每次执行都会转成字节码(byteCode)。于是乎,v8 会经过 TurboFan 操作转成优化的机器码(MachineCode),这样一来,可以减少许多不必要的操作,从而大大提升效率。
- Parse 将 JavaScript 代码转化为 AST,这是因为解释器并不直接认识 JavaScript 代码
- 如果函数没有被调用,那么是不会被转化成 AST 的;
- Parse 的 V8 官方文档:v8.dev/blog/scanne…
- Ignition 解释器,会将 AST 转化成 ByteCode
- 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算;
- 如果函数只调用一次,Ignition 会解释执行 ByteCode
- Ignition 的 V8 官方文档:v8.dev/blog/igniti…
- TurboFan 编译器,可以将字节码编译为 CPU 可以直接执行的机器码
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转化成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转化成字节码;
- TurboFan 的 V8 官方文档:v8.dev/blog/turbof…