书接上回~
系列文章目录:
为什么你觉得偶尔看浏览器的工作原理,但总是忘呢😵💫,因为你没有形成一个完整的知识网络,你的记忆是碎片化的。正如人的神经网络,只有当你的记忆相互依赖,相互链接,才能形成长期稳定的记忆。
所以本系列文章我将用一条知识线将浏览器工作原理的知识串联起来,因为本文的目的是为了帮助大家建立浏览器基础的思维树,所以很多细节点不做过多阐述,先有了树,后面你在上面伸展枝叶就会发现清晰明了很多。欢迎点赞支持或评论指正。
先来大概了解下渲染进程的职责:
以前人们常把浏览器内核分为渲染引擎和 Javascript 引擎。后面有了更明确的区分,浏览器内核单指渲染引擎,Javascript 引擎独立了出来。
所以浏览器内核,也就是渲染引擎,也可以叫排版引擎。浏览器内核是浏览器最核心的部分,负责对网页语法的解释并渲染(显示)网页。
Javascript 引擎的主要工作是将Javascript代码转换为快速优化的机器码,以便浏览器或服务器能够解释和执行。另外它还负责执行代码、分配内存以及垃圾回收。
| 浏览器 | 内核 | 厂商 | 兼容前缀 | 备注 | JS引擎 |
|---|---|---|---|---|---|
| Chrome | webkit > Blink | -webkit- | 2008年以前chrome用的是webkit内核,之后改用的Blink其实是webkit的分支 | V8 | |
| Safari | WebKit | Apple | -webkit- | 其实Safari才是WebKit内核的鼻祖,只是Chrome 广为人知且对WebKit有所贡献,所以一说到webkit,第一时间想到的是chrome | Nitro Javascript |
| IE、Edge | Trident > EdgeHTML | 微软 | -ms- | 国内很多浏览器都使用了Trident和Blink双内核,例如360、uc | JScript/Chakra |
| FireFox | Gecko | mozilla基金会 | -moz- | SpiderMonkey > TraceMonkey > JaegerMonkey | |
| Opera | presto > webkit > Blink | 挪威Opera Software | -o- | Linear A/ Linear B/ Futhark/ Carakan |
另外,在移动端,还有UC浏览器的u3内核,它是首个中国创造的浏览器内核,由UC研发团队耗时三年时间打造而成。以及腾讯系App内置webview(例如qq浏览器)的x5内核。这俩其实也是基于webkit内核改造的。
上一篇文章说到:浏览器渲染进程有5大类线程:GUI渲染线程、JS引擎线程、事件触发线程、定时器线程、异步HTTP请求线程。
从名称其实也能看出来,GUI渲染线程是基于渲染引擎工作的,JS引擎线程是基于 JS 引擎工作的,而其他三个线程是浏览器内部机制在处理。
所以说浏览器渲染进程与浏览器引擎之间的关系是协同工作的关系,共同实现了浏览器的核心功能。
首先我们要知道,js、css、图片等静态资源文件都是在网络进程中进行下载的。并且网络进程具有 并行下载 的能力,能够同时下载多个文件。而浏览器在解析页面之前,会启动一个 预解析 的线程,和网络进程通信,提前开始并行下载引入的外部 css、js文件。
另外,浏览器针对同一个域名内资源请求的并行连接数量有限制(为了防止DDOS攻击),所以一般网站为了加速资源下载,会做域名分散,这里就不多介绍了,想了解的可以看我以前写的一篇文章:域名发散
上一篇文章中我提到了,因为GUI渲染线程和JS引擎线程都需要访问和操作Dom,为了线程安全所以它们设计为互斥机制,即JS引擎线程工作时,GUI线程就会挂起,所以可以得出结论:js的加载会阻塞页面渲染。
那怎么就能不让 js 文件加载阻塞渲染呢?很简单,只需要把js文件放在最后加载或者在script标签上加上async或者defer属性的话,js加载就能变成异步的,不阻塞渲染,使用如下:
<script async src="https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.cjs.js"></script>
<script defer src="https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.cjs.js"></script>
那它们俩有什么区别呢?
首先我们要搞清楚一个概念,js的下载是无法阻止的。并且正如上面所说,js是可以并行下载的,所以js只是同步执行,并非下载也是同步的,很多网上的文章描述会让人引起误会,总是说async和defer可以把js下载和运行变成异步的。设置async、defer属性只是影响了js文件的执行时机,对下载并无影响。
DOMContentLoaded事件之前执行。css文件的加载不会阻塞Dom树的构建,因为Dom树和css规则是并行解析的,互不影响。但是css加载会阻塞渲染树的合成,所以css加载也会阻塞渲染。
document.styleSheets 查看除了内联和默认样式之外的所有内部和外部样式表**;display: none的元素会在渲染树中去除。red会变成rgb(255,0,0);相对单位会变成绝对单位,比如rem会变成px。日常开发想查看我们预设值计算出来的实际值,可以点击属性右键查看计算得出的值,当然你也可以切换计算样式面板查看全部属性计算后的值。
布局: 然后计算DOM树中可见元素的几何位置(例如节点的宽高、相对包含块的位置),生成布局树;
分层: 渲染主线程会对整个布局树中进行分层。一般滚动条、a标签、transform、will-change等样式都会影响分层效果,另外,像opacity、filter等属性虽说也能影响分层,但直接设置一般不会有效,需要设置动画animation才会独立分层。所以如果你想查看哪些元素单独分层了,可以在控制台的图层面板查看(设置这些属性的元素分层渲染可以触发GPU硬件“加速”):

绘制: 渲染引擎将将页面内容绘制到帧缓冲区(Framebuffer)中,帧缓冲区是一个内存区域,用于存储图像数据,这些图像数据最终会被 GPU 渲染到屏幕上。至此,渲染主线程的工作已经完成了,接下来由其他线程处理显示图像;
分块: 合成器线程首先对每个图层进行分块,将其划分为更多的小区域;
光栅化: 上面我们已经获得了文档结构、元素的样式、元素的几何关系、绘画顺序,接下来把这些信息转化为显示器中的像素才能显示,这个转化的过程,就叫做光栅化。此过程是合成器的光栅工作线程把每个块变成位图,位图可以理解成内存里的一个二维数组,这个二维数组记录了每个像素点信息;
合成: 合成器线程再将以上的像素信息生成合成帧,合成帧就是页面一个帧的内容的绘制四边形集合(绘制四边形是包含图块在内存的位置以及图层合成后图块在页面的位置之类的信息);
显示: 最后合成器线程会把出现在视口区的合成帧提交给浏览器进程,即渲染帧,最后浏览器进程将渲染帧发送给GPU从而展示在屏幕上,如果有页面滚动,则合成器线程会构建另外一个合成帧发送给GPU来更新页面;
额外补充:
先从两个角度简单区分下什么是 CPU 和 GPU:
通用处理器,主要用于执行各种计算任务,例如处理操作系统、运行应用程序、执行算法等;图形处理器,主要用于处理图形和图像相关的计算任务,例如图形渲染、图像处理、视频解码等;处理复杂的、多变的计算任务;大规模的、高度并行、重复的计算任务;以往网页渲染时,网页渲染过程中,渲染引擎对页面元素的样式布局计算、绘制等操作,通常都是在 CPU 上进行的。只有在最后显示时,GPU 才会工作处理像素信息显示网页。
不过目前主流浏览器都已经支持 CPU 或者 GPU 进行光栅化、合成操作了。例如 Chrome浏览器。默认是开启 GPU 加速,即由 GPU 处理分块、光栅化、合成操作(当然 GPU 加速带来的好处不止这些,还有例如3D图形加速和视频硬件解码加速,以及 GPU 和 CPU 并行带来的性能优化)。

开启 GPU 加速之后,我们可以打开控制台的渲染面板查看 GPU 的内存情况和帧渲染的信息,如下:


如果你想查看 GPU 的相关信息和状态,也可以使用 Chrome 浏览器的 chrome://gpu 页面。在里面可以查看 GPU 驱动程序的版本、GPU 加速是否已启用等信息,如果没有启用,会显示警告和错误信息,可以根据这些信息排查一些问题。
PS: 渲染面板功能非常多,例如在这里可以开启“图层边框”,也可以查看网页的分层情况。还可以利用“布局偏移区域”,开启后排查网页会引起回流的元素,以优化网页渲染问题。
css硬件加速,简单来说,就是使用某些 css 属性后,触发 gpu 工作加速渲染指定元素。
这里的 GPU 硬件加速和上一节中设置开启 GPU 加速是两个概念。上面开启 GPU 硬件加速是把页面渲染的一些图形操作环节移至 GPU 处理,减轻 CPU 压力,优化渲染性能,而这一节要说的 GPU 加速是把指定元素绘制到独立的图层,由 GPU 完成所有计算,这样不仅加速了图形渲染,还不会造成页面的回流重绘,从而优化了动画性能。
例如我设置了一个背景色变化的动画,可以看到性能面板的底部摘要饼图,渲染、绘制时间一直在增加。而一个透明度opacity变化的动画,渲染、绘制时间是不会增加的。因为opacity动画触发了css硬件加速。


总结: GPU 硬件加速的原理有两个原因:
这里主要说下V8引擎解析的工作流程:
Ignition),它将AST转换为字节码。字节码是一种中间表示,不同于机器代码,但比源代码更容易解释和执行。TurboFan)来将字节码转换为本机机器代码。本机机器代码的执行速度更快,因此通过JIT编译可以提高性能。下面是我画的一张流程图:

V8 引擎采用了一种称为 “混合执行(Hybrid Execution)” 的策略,将解释执行和即时编译执行结合起来,以在不同情况下实现最佳的性能和响应时间。简单来说就是判断某段代码是否频繁执行,并且有利于性能提升,如果是,则交给TurboFan即时编译器处理,否则还是由解释器执行。
除了对热点函数使用即时编译外,js引擎还有一些其他的优化手段,例如:
内联缓存(Inline Caching,简称IC) :V8 引擎会根据对象的类型和属性访问的上下文动态地生成内联缓存,以避免不必要的查找操作,从而提高访问速度。简单来说,就是在 V8 执行代码时,会把函数中的一些关键数据缓存起来,下次执行该函数时就可以节省获取这个数据的时间了,以提升一些重复代码的执行效率;
预解析(Pre-Parsing) :V8 引擎可以在执行 JavaScript 代码之前对其进行预解析,以提前分析代码结构和语法,并生成相应的解析树和抽象语法树(AST)。这可以加速代码的执行过程,尤其是在代码需要频繁执行的情况下。
浏览器的垃圾回收机制是一种自动管理内存的机制,用于检测和释放不再使用的内存,以减少内存泄漏和提高系统性能。
内存分配和释放的过程(生命周期)分为以下几个阶段:
堆内存(Heap)和栈内存(Stack)是计算机内存的两个主要区域,它们分别用于存储不同类型的数据,
垃圾回收机制来回收不再使用的内存。堆的大小通常比栈大,并且可以动态增长和收缩。动态数据存储在堆内存中,同时会把其内存地址存到栈内存中。所以如果一个对象的引用存储在栈内存中,即使执行上下文被弹出,这个对象仍然存在于堆内存中,只要还有其他引用指向它,它就不会被垃圾回收机制清除。
引用计数法:每个对象维护了一个引用计数器,记录着当前有多少个指针指向该对象。当引用计数器减为零时,说明该对象不再被引用,可以被释放。
优势:
缺点:
看一个例子,就能很鲜明的看出引用计数存在的缺点了:
function foo() {
const A = {};
const B = {};
A.foo = B;
B.foo = A;
return "hello abin";
}
foo();
很明显,上面函数 foo() 内创建了两个对象 A 和 B,并相互引用了对方,形成了一个循环引用。这样即使foo函数执行完,A、B的引用数也不会变为0,就会造成内存泄漏。
解决办法:手动把变量设置为null
标记清除法:从根对象(通常是全局对象,可以理解为windows)开始,遍历内存中所有对象的引用关系,如果是能访问到的对象,则标记为可达对象(无法访问的为不可达对象),标记所有可达对象,最后清除未被标记的对象,实现内存的自动回收。
优点:
缺点:
内存碎片化(内存零零散散的存放,造成资源浪费);优化:
为了优化标记清除法内存碎片化的问题,通常会在标记后引入整理阶段,将存活的对象整理到一起,以释放出连续的内存空间,提高内存的利用率。
整个过程为:
标记

整理

清除

V8引擎的垃圾回收机制采用了分代回收策略,将堆内存中的对象按照存活时间分为不同的代(Generation),通常分为新生代(Young Generation)和老生代(Old Generation)两个代。然后这两代垃圾采用不同的垃圾回收机制处理。

新生代存放的是存活时间较短的对象(经过一次垃圾回收后,就被释放回收掉),由副垃圾回收器管理,通常使用复制算法(Copying Algorithm)来进行垃圾回收。
回收流程:
新生代晋升老生代机制:
新生代存放的是存活时间较长的对象(经过多次垃圾回收后仍存在),由主垃圾回收器管理,通常使用标记-整理-清除法来进行垃圾回收。
Orinoco是目前v8引擎的垃圾回收器,因为垃圾回收存在全停顿问题(在进行垃圾回收操作时,整个应用程序的执行都会被暂停),可能会导致页面卡顿,所以Orinoco采用了一些优化手段。
当然了,新生代占用内存较小,活动对象也比较少,所以全停顿的影响不大,以下优化手段主要是针对老生代的:
1. 并行垃圾回收
启用多个辅助线程来并行进行垃圾回收,缩短回收时间;
2. 增量垃圾回收
垃圾回收和代码执行交替进行,减小阻塞,但是在gc停顿时,如何能够从暂停的地方继续遍历呢?Orinoco主要采用了下面两个方法:
3.并发垃圾回收
主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作。可以理解为异步的垃圾回收策略,同时为了解决增量标记的问题,也需要进行写屏障操作。
4.惰性清理:
在增量标记之后,如果剩余的内存空间足以让JS代码跑起来,就会延迟清理,先让JS代码执行,或者只清理部分垃圾,而不清理全部。
内存泄漏指的是程序中未释放不再需要的内存的情况。
const mySet = new Set();
const obj = { key: 'value' };
mySet.add(obj);
// 在不再需要 obj 时,手动删除它
mySet.delete(obj);
const weakSet = new WeakSet();
const obj = { key: 'value' };
weakSet.add(obj);
// 不再需要 obj 时,WeakSet 会自动处理
利用 Chrome 的 DevTools 可以很容易排查内存泄漏问题。主要是用 Performance性能 面板和 Memory内存 面板。
1. 利用 Performance 工具排查是否存在内存泄漏问题
打开 Performance 面板,勾选内存选项(默认是不勾选的),即可开始收集内存随时间的变化曲线,如下图中框选的蓝色趋势部分,如果该趋势走向趋于平稳,则内存回收正常,否则即可能存在内存泄漏问题。
内存选项旁边的扫帚图标可以手动进行GC(垃圾回收)

2. 利用 Memory 面板定位问题
Memory 面板有三个选项:堆快照(Heap Snapshot)、内存时间轴(Memory Timeline)、内存分配采样(Allocation)
一般常用堆快照和内存时间轴。
堆快照可以捕获网页的内存快照,并提供详细的内存信息和统计数据。如果你已经大概猜到了哪里导致了内存泄漏,可以在操作前后捕获内存快照,并进行比较,在增量 > 0点记录中定位问题。

而内存时间轴可以显示网页在时间轴上的内存使用情况,在时间轴上,可以看到有起伏的蓝色和灰色柱状图,其中蓝色代表当前时间线下所占用的内存;灰色表示表示原占用空间得到释放。
录制一段时间之后,结束录制,同样会生成快照。比堆快照更方便的是,你可以查看各个时间段的内存数据以排查问题。

当然你也可以查看最终的内存分配情况定位问题:

3. 需要关注的Constructor构造函数
可以看到,在内存面板中,堆内存列表列出了很多构造函数,为了快速定位问题,你需要了解下这些常见的构造函数大致代表什么:
学如逆水行舟,不进则退~加油吧少年👊👊👊
先看后赞,养成习惯👍
收藏吃灰,不如学会🍗
点个关注,不要迷路🪤