过程、JavaScript 引擎解析 JavaScript 代码的过程"/>
《十》浏览器基础及渲染引擎解析一个网页的过程、JavaScript 引擎解析 JavaScript 代码的过程
浏览器:是安装在电脑里面的一个软件,能够将页面内容渲染出来呈现给用户查看,并让用户与网页进行交互。
服务器其实就是性能比较高的计算机,这些计算机 24 小时不断电、 不关机。
开发者在本地开发出 HTML 网页文件、CSS 样式文件、JS 脚本程序等,然后上传给服务器进行存储和共享,用户就可以访问到这些资源了。例如;将test.html
文件上传到百度服务器的 public 文件夹下,此时这个文件就拥有了网址:.html
,所有用户访问这个网址都可以看到开发者开发出来的网页。
输入 URL,浏览器访问一个网页的过程:
- 在浏览器地址栏中输入网址,按下回车。
- 浏览器向 DNS 域名解析服务器发出解析域名的请求。
- DNS 域名解析服务器将域名解析为对应的 IP 地址,并返回给浏览器。
- 浏览器根据 IP 地址与目标服务器建立 TCP 连接。
- 浏览器发送请求报文给服务器 。
- 服务器接收请求报文后处理浏览器请求,发送响应报文将处理结果返回给浏览器。
- 浏览器接收响应报文,解析内容呈现给用户。
- 收发报文结束,释放TCP连接。
通过浏览器访问某个网页,服务器会返回对应的配置好的 HTML 文件,浏览器将 HTML 文件下载完成后进行解析,解析的过程中发现有 link 元素、script 元素、image 元素、font 元素等,再向服务器请求 CSS 文件、JS 文件、图片资源、字体资源等。
上述浏览器访问一个网页的过程中,不考虑浏览器有这个网页的 DNS 缓存和资源缓存的情况。
有 DNS 缓存的话,就不需要再去 DNS 服务器解析域名了,直接使用 DNS 缓存中的 IP 地址即可。
有资源缓存的话,就可以直接使用缓存资源或者只需要再向服务器确认一下资源是否过期可用即可。
第二次访问网页要比第一次快,是因为第一次访问时已经将这个网页的信息缓存到了本地。
缓存文件夹除了缓存图片以外,还缓存了一些.js
、.css
、.html
等文件,所以一个网页不是一个文件,而是一堆文件。网页越复杂那么组成这个网页的文件就越多。
浏览器内核:
浏览器内核本来是分成渲染引擎和 JS 引擎两部分,但是由于 JS 引擎越来越独立,内核就倾向于只指渲染引擎了。渲染引擎用来解析 HTML 和 CSS,渲染网页。
正是因为内核不同,浏览器才有兼容问题。
渲染引擎用来渲染页面,JS 引擎用来执行 JS。
常见的主流浏览器及其内核:
常见的主流浏览器有:Chrome、Edge、Firefox、Safari、Opera。
- Chrome 浏览器以前是 Webkit 内核,现在是 Blink 内核。
- Edge 浏览器以前是 Webkit 内核,现在是 Blink 内核。
Edge 浏览器是微软研发的,在 Windows10 中取代 IE 浏览器成为默认浏览器。
IE 浏览器是 Trident 内核,在 2023 年 2 月 14 日永久停用。 - Firefox 浏览器是 Gecko 内核。
- Safari 浏览器是 Webkit 内核。
- Opera 浏览器最初是自己的 Presto 内核,后来是 Webkit,现在是 Blink 内核。
UC 浏览器、百度浏览器是 Trident 内核。
360 浏览器、搜狗浏览器是 Trident + Webkit 双内核。和钱相关的会自动选择 Trident 内核,安全性高一些;其余时候会只能选择 Webkit 运行。
浏览器的渲染引擎:
浏览器的渲染引擎解析一个网页的过程:
-
HTML --> HTML Parser --> DOM Tree
:渲染引擎对 HTML 从上到下进行解析,生成 DOM 树。
-
Style Sheets --> CSS Parser --> Style Rules
:在解析 HTML 的过程中,如果发现有 CSS,对其进行解析,生成 CSSOM 规则树。CSS 的下载和解析不会阻塞 HTML 的继续解析。
-
Attachment:将解析完成的 CSSOM 规则树应用到 DOM 树上生成渲染树。在渲染树上有记录每个节点其他的样式信息,但是没有记录大小和位置信息的。
只有 DOM 树和 CSS 规则都解析完成,才能生成渲染树。
也就是说, CSS 文件的下载和解析不会影响 DOM 树的生成,但是会影响渲染树的生成。DOM 树和渲染树可能不是一一对应的。比如:某个元素的 CSS 属性是
display:none
,默认不需要渲染出来。这个元素在 DOM 树上是存在的,在渲染树上是不存在的。为了达到更好的用户体验,浏览器的渲染引擎会尽快将已经构建好的一部分内容显示在屏幕上,而不必等到整个 HTML 文档都解析完毕之后才开始构建 render 树和布局进行绘制。
- Layout:计算布局样式,得出渲染树上的每个节点的大小信息和位置信息,生成绝对的坐标。
- Painting:将渲染树中的每个节点转为实际的像素点绘制到屏幕上,显示出来。
浏览器的回流与重绘:
回流一定会引起重绘,非常消耗性能;重绘不一定会引起回流,性能消耗相比回流来说要小。
回流 reflow(重布局、重排):
第一次计算节点的大小和位置,称为布局。之后修改了节点的大小和位置重新计算,称为回流。
由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但 table 及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,这就是应该尽量避免使用 table 布局页面的原因之一。
引发回流的操作:
- DOM 树结构发生变化:添加节点、删除节点。
- 改变了布局:修改元素的尺寸、位置信息。
- 浏览器窗口 resize。
- 获取元素的一些属性时:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle() 等。
当获取这些全局属性时,需要此时页面上的其他元素的布局和样式处于最新状态,这会引起多次的回流和重绘,称为强制回流。
回流优化的方法:
- 修改元素的大小、位置时尽量一次性修改。
例如:要将 box 的宽改为
100px
,高改为200px
,最好不要使用box.style.width = '100px'; box.style.height = '200px';
这种方式一个一个修改,可以通过动态添加 class 的方式一次性修改。 - 尽量避免频繁地 DOM 操作:可以在一个 DocumentFragment 或者父元素中将要执行的 DOM 操作都执行完,再一次性地添加到 DOM 上。
- 脱离文档流的元素的变化不会影响到其他元素,可以将需要多次重排的元素设置
position: absolute
或者position: fixed
属性来减少对其他元素的影响。 - 由于属性为
display: none
的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。所以,如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示,这样只在隐藏和显示时触发两次重排。
重绘 repaint:
第一次将渲染树中的每个节点转为实际的像素点绘制到屏幕上,称为绘制;之后重新渲染,称为重绘。
引发重绘的操作:修改元素的外观,例如文字颜色、背景颜色、边框颜色、边框样式等。
浏览器的 Composite 合成:
浏览器在 Painting 绘制的过程中,还可以进行 Composite 合成,这是浏览器提供的一种优化手段。
浏览器可以将布局后的元素绘制到多个图层中。video
、canvas
、iframe
元素,设置了 position: fiexed
、3D transforms
、animation、trasnition 时设置 opacity、transform
、will-change
(一个实验性的属性。提前告诉浏览器元素可能发生哪些变化) 属性的元素,都会被绘制到一个新的合成层中。而其他的元素都会被绘制在同一个图层中。
合成层可以提升性能,因为每个合成图层都是单独渲染的,并且会交给 GPU 来处理,比 CPU 更快;而且当需要重绘时,合成层只需要重绘本身,不会影响到其他的图层。
可以在 chrome 开发者工具中查看图层。
<style>.box {width: 100px;height: 100px;}.box1 {background: red;}.box2 {background: green;position: fixed;}.box3 {background: blue;position: fixed;}
</style><div class="box box1"></div>
<div class="box box2"></div>
<div class="box box3"></div>
可以看到有三个图层,其中两个是合成层。
对于有动画效果的元素,可以提升为合成层,来减少重绘对其他元素的影响,提升性能。虽然合层成确实可以在某种程度上提高性能,但是是以内存管理为代价的,不应作为 Web 性能优化策略的一种方式来过度使用。
<script>
元素和页面解析的关系:
默认情况下,在遇到 <script>
元素时,JS 的下载和执行会阻塞 HTML 的解析,只有下载好并执行完脚本才会继续解析 HTML。因为 JS 的作用之一就是操作 DOM,如果等到 DOM 树构建完成并渲染之后再执行 JS,JS 修改到 DOM 的话会造成严重的回流和重绘,影响页面的性能。
JS 的下载和解析会阻塞 HTML 的继续解析。
CSS 的下载和解析不会阻塞 HTML 的继续解析。
为了解决这个问题,除了可以将 <script>
元素放到 <body>
的最底部外,<script>
元素还提供了两个属性:defer 和 async。
- defer:只适用于外部脚本。JS 脚本会立即在新的线程中单独下载,不会阻塞 HTML 的解析;但是脚本下载完成后,不会立即执行,会等待 HTML 解析完成之后,DOMContentLoaded 事件发出之前再执行。多个脚本会按照在 HTML 文档中的顺序依次执行。
- async:只适用于外部脚本。JS 脚本会立即在新的线程中单独下载,不会阻塞 HTML 的解析;但是脚本下载完成后,会立即执行,此时会阻塞 HTML 的解析。多个脚本的执行顺序无法确定。
在设置了 defer 属性的 JS 文件中,访问到 DOM 元素一定是存在的;在设置了 async 属性的 JS 文件中,访问到 DOM 元素不一定存在。
JS 引擎:
JS 引擎用来执行 JS。因为计算机是不能直接识别 JS 这种高级语言的,只能识别机器语言,所以需要 JS 引擎来将其翻译成机器语言。
比较著名的 JS 引擎有:Google 谷歌的 V8、Mozilla 的 SpiderMonkey、Apple 苹果的 JavaScriptCore、Microsoft 微软的 Chakra。
由于现在 JS 是可以在浏览器外使用的,因此 JS 引擎可以独立于浏览器单独使用。
计算机是一种根据指令操作数据的机器,主要由存储器与处理器两部分组成。存储器又称 RAM(随机存取存储器),用于存储指令以及需要操作的数据;处理器又称 CPU(中央处理器),它从存储器获取指令与数据,并执行相应的计算。
机器语言是用二进制代码 0101 表示的计算机能直接识别和执行的一种机器指令。运行速度最快,但是难以编写、晦涩难懂。
为了减轻程序设计人员的负担,很快出现了汇编语言,这种语言是用符号来代表二进制代码的,其实就相当于助记符,用这种语言编写的程序需要通过编译器将其编译为机器语言后,计算机才能执行。
不同类型的 CPU 的机器指令系统是不同的,意味着编写的机器语言和汇编语言一般只能在同类型的 CPU 上运行。
高级语言能在任何计算机上运行,但是需要先将其翻译成机器语言。
V8 引擎:
V8 引擎是谷歌使用 C++ 开发的一种开源的、高性能的 JS 引擎,应用在 Chrome 浏览器 和 Node.js 上。
V8 引擎解析 JS 代码的过程:
JS 引擎对 JS 代码的处理包括解析和执行两个阶段。JS 代码的解析和执行并不是一次性完成的,而是交替进行的。当 JS 引擎执行代码时,它可能需要解析新的代码;反之,当它在解析阶段时,也可能会执行一些代码。
本章主要记录解析的过程,执行的过程在 《JavaScript 引擎执行 JavaScript 代码的过程:执行上下文、执行上下文栈》一章。
-
Parse:解析器。对 JS 代码进行解析,转换成 AST 抽象语法树。
抽象语法树:用来表示 JS 源码树形结构的对象。
整个解析过程可以分为两部分:词法分析和语法分析。
词法分析:将每条语句进行分词处理,把每个词转换成一个 token 记号。
语法分析:对每个词进行解析,解释成真正的语法。 -
Ignition:解释器。根据 AST 抽象语法树生成字节码,并逐行一边解释一边执行字节码;同时会收集 TurboFan 优化所需要的信息(比如:函数参数的类型信息,有了类型才能进行真实的运算)。
字节码是一种需要直译器转译后才能称为机器码的中间代码。字节码是跨平台的。
机器码(机器语言)是电脑 CPU 直接读取运行的机器指令。编写的机器码一般只能在同类型的 CPU 上运行。
Ignition 解释执行字节码时,最终也是会将其转成机器码的。 -
TurboFan:编译器。如果一段代码被重复执行,那么 TurboFan 会把这段代码的字节码编译为 CPU 可以直接执行的机器码,当再次执行这段代码的时候,直接执行编译后的机器码就可以了,大大提高了代码的执行效率。
没有 TurboFan:对于重复执行的 JS 代码,Ignition 重复解释、执行其字节码。
有 TurboFan:对于重复执行的 JS 代码,第一次 Ignition 解释、执行其字节码;之后直接执行其机器码。function sum(num1, num2) {return num1 + num2 } sum(10, 20) sum(20, 30) sum(30, 40)
sum 函数重复执行。
如果没有 TurboFan,那么就需要 Ignition 重复地逐行解释执行 sum 函数的字节码,性能是比较低的。
有了 TurboFan 之后,TurboFan 会将 sum 函数的字节码编译为 CPU 可以直接执行的机器码,当再次执行 sum 函数的时候,就不再需要 Ignition 逐行解释执行 sum 函数的字节码了,直接执行编译后的机器码就可以了,大大提高了代码的执行效率。
所以,在编写函数的时候,尽量保证参数类型不要改变,可以提高性能。 -
Deoptimization:如果执行重复代码的过程中,发现它发生了变化,之前优化的机器码不能正确地处理运算,就会 Deoptimization 反优化,逆向地将机器码还原为字节码,
如果一个函数没有被调用,那么是不会被转换成 AST 抽象语法树的。
如果一个函数只被调用 1 次,那么 Ignition 将其编译为字节码后就直接解释执行了,TurboFan 不会对其进行优化编译。因为 TurboFan 需要 Ignition 收集函数执行时的类型信息,这就要求函数至少需要执行 2 次,TurboFan才有可能进行优化编译。
如果一个函数被调用多次,那么它会被标记为热点函数,如果 Ignition 收集的类型信息证明它可以进行优化编译的话,TurboFan 会将字节码转换成优化之后的机器码,以提高代码的执行性能。
V8 引擎的垃圾回收器:
V8 引擎中最核心的四个模块,除了 Parse 解析器、Ignition 解释器、TurboFan 编译器外,还有垃圾回收器。
更多推荐
《十》浏览器基础及渲染引擎解析一个网页的过程、JavaScript 引擎解析 JavaScript 代码的过程
发布评论