本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
🌻 前言
书接上回~
系列文章目录:
- # ✊构建浏览器工作原理知识体系(开篇)
- # ✊构建浏览器工作原理知识体系(浏览器内核篇)
- ✊构建浏览器工作原理知识体系(网络协议篇) 待更新
- ✊构建浏览器工作原理知识体系(网页加载超详细全过程篇) 待更新
为什么你觉得偶尔看浏览器的工作原理,但总是忘呢😵💫,因为你没有形成一个完整的知识网络,你的记忆是碎片化的。正如人的神经网络,只有当你的记忆相互依赖,相互链接,才能形成长期稳定的记忆。
所以本系列文章我将用一条知识线将浏览器工作原理的知识串联起来,因为本文的目的是为了帮助大家建立浏览器基础的思维树,所以很多细节点不做过多阐述,先有了树,后面你在上面伸展枝叶就会发现清晰明了很多。欢迎点赞支持或评论指正。
🪴一、渲染进程的工作
先来大概了解下渲染进程的职责:
- HTML 解析: 渲染进程负责解析 HTML 文档,构建 DOM 树;
- CSS 解析: 解析样式表,生成 CSS 规则树(Style Rules / CSSOM );
- 合成渲染树: 将 DOM 树和 CSS 规则树合成为渲染树(Render Tree),该树只包含需要渲染的节点(会去除display:none的元素);
- 布局(Layout): 计算每个渲染树节点的几何信息,包括位置和大小,生成布局树;
- 绘制(Paint): 将渲染树的每个节点绘制到屏幕上,创建位图(Bitmap);
- 合成(Composite): 将各个图层按正确的顺序(根据层叠顺序z-index和透明度等因素)合成为最终的页面画面,提高绘制性能;
- 处理用户输入: 监听用户输入事件(鼠标点击、键盘输入等),并触发相应的事件处理程序;
- 脚本执行: 执行页面中的 JavaScript 脚本,响应用户交互、更新页面内容;
- 处理网络请求: 处理页面发起的网络请求,包括获取 HTML、CSS、JavaScript 文件等;
- 插件管理: 如果页面使用了插件(如 Flash、PDF 阅读器等),渲染进程会与插件进行交互;
- 内存管理: 管理渲染进程的内存使用,确保不发生内存泄漏等问题;
- 安全性: 实施浏览器的安全策略,防止恶意脚本和攻击;
🪴二、浏览器内核和JS引擎
以前人们常把浏览器内核分为渲染引擎和 Javascript 引擎。后面有了更明确的区分,浏览器内核单指渲染引擎,Javascript 引擎独立了出来。
所以浏览器内核,也就是渲染引擎,也可以叫排版引擎。浏览器内核是浏览器最核心的部分,负责对网页语法的解释并渲染(显示)网页。
Javascript 引擎的主要工作是将Javascript代码转换为快速优化的机器码,以便浏览器或服务器能够解释和执行。另外它还负责执行代码、分配内存以及垃圾回收。
(一)常见的浏览器内核和JS引擎
浏览器 | 内核 | 厂商 | 兼容前缀 | 备注 | 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攻击 ),所以一般网站为了加速资源下载,会做域名分散,这里就不多介绍了,想了解的可以看我以前写的一篇文章:域名发散
(一)JS文件加载阻塞
上一篇文章中我提到了,因为GUI渲染线程和JS引擎线程都需要访问和操作Dom,为了线程安全
所以它们设计为互斥机制,即JS引擎线程工作时,GUI线程就会挂起,所以可以得出结论:js的加载会阻塞页面渲染。
那怎么就能不让 js 文件加载阻塞渲染呢?很简单,只需要把js文件放在最后加载或者在script标签上加上async
或者defer
属性的话,js加载就能变成异步的,不阻塞渲染,使用如下:
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文件的执行时机,对下载并无影响。
- async:js文件异步执行,哪个先下载完哪个先执行
- defer:延迟执行,在文档被解析后,并且触发
DOMContentLoaded
事件之前执行。
(二)CSS文件阻塞渲染
css文件的加载不会阻塞Dom树的构建,因为Dom树和css规则是并行解析的,互不影响。但是css加载会阻塞渲染树的合成,所以css加载也会阻塞渲染。
🪴八、垃圾回收机制
浏览器的垃圾回收机制是一种自动管理内存的机制,用于检测和释放不再使用的内存,以减少内存泄漏和提高系统性能。
(一)内存生命周期
内存分配和释放的过程(生命周期)分为以下几个阶段:
- 分配内存:当我们申请变量、函数、对象的时候,系统会自动为它们分配内存;
- 内存使用:即读写内存,也就是使用变量、函数等;
- 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存;
(二)栈内存、堆内存
堆内存(Heap)和栈内存(Stack)是计算机内存的两个主要区域,它们分别用于存储不同类型的数据,
- 堆(Heap): 堆是用于动态分配内存的区域,用于存储引用类型的数据,如对象和数组。在堆中分配的内存不会自动释放 ,需要通过
垃圾回收机制
来回收不再使用的内存。堆的大小通常比栈大,并且可以动态增长和收缩。 - 栈(Stack): 栈是用于管理函数执行上下文和存储基本类型值的一种数据结构,其分配和释放由执行上下文(Execution Context)的进入和退出决定。
动态数据存储在堆内存中,同时会把其内存地址存到栈内存中。所以如果一个对象的引用存储在栈内存中,即使执行上下文被弹出,这个对象仍然存在于堆内存中,只要还有其他引用指向它,它就不会被垃圾回收机制清除。
(三)两种基础的垃圾回收机制
1. 引用计数法
引用计数法:每个对象维护了一个引用计数器,记录着当前有多少个指针指向该对象。当引用计数器减为零时,说明该对象不再被引用,可以被释放。
优势:
- 可即刻回收垃圾,当被引用数值为0时,对象在变成垃圾的时候就立刻被回收。
- 因为是即时回收,所以程序不会暂停去单独使用很长一段时间的GC,那么最大暂停时间很短。
缺点:
- 时间开销大,因为引用计数算法需要维护引用数,一旦发现引用数发生改变需要立即对引用数进行修改;
- 最大的缺点还是无法解决循环引用的问题;
看一个例子,就能很鲜明的看出引用计数存在的缺点了:
ini
function foo() {
const A = {};
const B = {};
A.foo = B;
B.foo = A;
return "hello abin";
}
foo();
很明显,上面函数 foo()
内创建了两个对象 A
和 B
,并相互引用了对方,形成了一个循环引用。这样即使foo
函数执行完,A、B的引用数也不会变为0,就会造成内存泄漏。
解决办法:手动把变量设置为null
2. 标记清除法(标记整理法、可达性分析法)
标记清除法:从根对象(通常是全局对象,可以理解为windows)开始,遍历内存中所有对象的引用关系,如果是能访问到的对象,则标记为可达对象(无法访问的为不可达对象),标记所有可达对象,最后清除未被标记的对象,实现内存的自动回收。
优点:
- 实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示;
- 解决了循环引用的问题;
缺点:
内存碎片化
(内存零零散散的存放,造成资源浪费);- 再分配时遍历次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端;
- 不会立即回收资源;
优化:
为了优化标记清除法内存碎片化的问题,通常会在标记后引入整理阶段,将存活的对象整理到一起,以释放出连续的内存空间,提高内存的利用率。
整个过程为:
标记
整理
清除
(四)V8引擎的垃圾回收机制(分代垃圾回收)
V8引擎的垃圾回收机制采用了分代回收策略,将堆内存中的对象按照存活时间分为不同的代(Generation),通常分为新生代(Young Generation)和老生代(Old Generation)两个代。然后这两代垃圾采用不同的垃圾回收机制处理。
新生代
新生代存放的是存活时间较短的对象(经过一次垃圾回收后,就被释放回收掉),由副垃圾回收器 管理,通常使用复制算法(Copying Algorithm)
来进行垃圾回收。
回收流程:
- 触发回收:新对象首先被分配到From空间中,当From空间被占满时,就会触发垃圾回收机制;
- 标记阶段:从根对象遍历对象引用关系,标记所有活动对象;
- 复制阶段:将所有活动对象从 From 空间复制到 To 空间,并且进行排序,使得To空间成为连续的内存块;
- 清除阶段:对 From 空间进行清理,回收非活动对象所占用的内存空间。
- 空间交换:清除完成后,From空间和To空间相互交换,即From空间变为To空间,To空间变为From空间。这样,下一次的垃圾回收就可以在新的To空间中进行。
新生代晋升老生代机制:
- 年龄达到阈值:每个对象都有一个年龄计数器,初始为0。每次经过一次垃圾回收,如果对象仍然存活,它的年龄计数器就会加1。当年龄计数器达到阈值时,对象就会被晋升到老生代内存;
- To空间的内存占用达到一定比例:当To空间的内存占用超过一定比例(通常是25%到50%)时,也会触发对象的晋升。这是为了避免新生代内存过快地被填满,导致频繁的垃圾回收。
老生代
新生代存放的是存活时间较长的对象(经过多次垃圾回收后仍存在),由主垃圾回收器 管理,通常使用标记-整理-清除法
来进行垃圾回收。
Orinoco优化
Orinoco
是目前v8引擎的垃圾回收器,因为垃圾回收存在全停顿
问题(在进行垃圾回收操作时,整个应用程序的执行都会被暂停),可能会导致页面卡顿,所以Orinoco采用了一些优化手段。
当然了,新生代占用内存较小,活动对象也比较少,所以全停顿的影响不大,以下优化手段主要是针对老生代的:
- 并行垃圾回收 (parallel)
- 增量垃圾回收 (incremental)
- 并发垃圾回收 (concurrent)
- 惰性清理
1. 并行垃圾回收
启用多个辅助线程来并行
进行垃圾回收,缩短回收时间;
2. 增量垃圾回收
垃圾回收和代码执行交替进行,减小阻塞,但是在gc停顿时,如何能够从暂停的地方继续遍历呢?Orinoco
主要采用了下面两个方法:
- 三色标记法: 常规标记标记清除法只有两种状态:活动对象(黑色)、未活动对象(白色)。三色标记有三种状态:活动对象(黑色)、已经访问过并正在处理的对象(灰色)、未活动对象(白色)
- 写屏障(强三色不变性): 当发生了黑色的节点引用了白色的节点,写屏障机制会强制将被引用的白色节点变成灰色的,这样就保证了下一次增量标记阶段可以正确标记;
3.并发垃圾回收
主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作。可以理解为异步
的垃圾回收策略,同时为了解决增量标记的问题,也需要进行写屏障
操作。
4.惰性清理:
在增量标记之后,如果剩余的内存空间足以让JS代码跑起来,就会延迟
清理,先让JS代码执行,或者只清理部分垃圾
,而不清理全部。
🪴九、内存泄漏
内存泄漏指的是程序中未释放不再需要的内存的情况。
(一)常见可能造成内存泄漏的问题:
- 不合理的闭包
- 被遗忘的定时器(setInterval)、requestAnimationFrame;
- DOM 引用丢失(DOM 被移除了,但是绑定在其身上的各类事件还在);
- 比如一些音视频的播放器,在不使用时,要及时的 destory 而非简单的移除其 DOM 元素;
- 未清理的console打印;
- 被忽视的全局变量引用;
- 第三方库的引用与销毁;
- ES6 的一些语法的使用:Map、Set 等。要及时清除引用或者使用弱引用的weakmap、weakset,例如下:
ini
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构造函数
可以看到,在内存面板中,堆内存列表列出了很多构造函数,为了快速定位问题,你需要了解下这些常见的构造函数大致代表什么:
- system、system/Context 表示引擎自己创建的以及上下文创建的一些引用,这些不用太关注,不重要
- closure 表示一些函数闭包中的对象引用
- array、string、number、regexp 这一系列也能看出,就是引用了数组、字符串、数字或正则表达式的对象类型
- HTMLDivElement、HTMLAnchorElement、DocumentFragment等等这些其实就是你的代码中对元素的引用或者指定的 DOM 对象引用
🎁 说在最后
学如逆水行舟,不进则退~加油吧少年👊👊👊
先看后赞,养成习惯👍
收藏吃灰,不如学会🍗
点个关注,不要迷路🪤