浏览器内核与 JS 引擎
一、什么是浏览器内核
浏览器内核也称为渲染引擎(Rendering Engine),决定了浏览器如何显示网页的内容以及页面的格式信息。
二、主流浏览器的内核
1、国内浏览器市场份额
2、五大主流浏览器的内核
浏览器 | 内核 | 说明 |
---|---|---|
IE | Trident | 微软的IE浏览器浏览器更新至 IE10 后,伴随着 WIN10 的上市,迁移到了全新的浏览器 Edge。除了 JS 引擎沿用之前 IE9 就开始使用的查克拉(Chakra),渲染引擎使用了新的内核 EdgeHTML(本质上不是对 Trident 的完全推翻重建,而是在 Trident 基础上删除了过时的旧技术支持的代码,扩展和优化了对新的技术的支持,所以被看做是全新的内核) |
FireFox | Gecko | 火狐的 JS 引擎历经 SpiderMonkey、TraceMonkey 到现在的 JaegerMonkey。其中 JaegerMonkey 部分技术借鉴了 V8、JSCore 和 Webkit,算是集思广益。 |
Chrome | Chromium / blink | Chrome 发布于 2008 年,使用的渲染内核是 Chromium,它把 Webkit 梳理得更有条理可读性更高,效率提升明显。2013年,由于 Webkit2 和 Chromium 在沙箱设计上的冲突,谷歌联手 Opera 自研和发布了 Blink 引擎,逐步脱离了 Webkit 的影响。所以,可以这么认为:Chromium 扩展自 Webkit 止于 Webkit2,其后 Chrome 切换到了 Blink 引擎。另外,Chrome 的 JS 引擎使用的 V8 引擎,应该算是最著名和优秀的开源 JS 引擎,大名鼎鼎的 Node.js 就是选用 V8 作为底层架构。 |
Safari | webkit | Safari 是 webkit 的鼻祖,Webkit 引擎包含 WebCore 排版引擎及 JavaScriptCore 解析引擎,均是从 KDE 的 KHTML 及 KJS 引擎衍生而来。Webkit2 发布于 2010 年,它实现了元件的抽象画,提高了元件的重复利用效率,提供了更加干净的网页渲染和更高效的渲染效率。另外,Webkit 也是苹果 Mac OS X 系统引擎框架版本的名称,主要用于 Safari、Dashboard、Mail。 |
Opera | blink | Opera 在 13 年 V12.16 之前使用的是 Opera Software 公司开发的 Presto 引擎,之后连同谷歌研发和选择 Blink 作为 Opera 浏览器的排版内核。 |
三、浏览器架构
1、浏览器内核的进程架构
浏览器主要的进程有 4 个:
- 浏览器进程(Browser Process):负责浏览器的 Tab 的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;
- 渲染进程(Renderer Process):负责一个 Tab 内的显示相关的工作,也称渲染引擎;
- 插件进程(Plugin Process):负责控制网页使用到的插件;
- GPU 进程(GPU Process):负责处理整个应用程序的 GPU 任务。
1-1、多进程架构的优点
假设打开了三个标签页,每个标签页都拥有自己独立的渲染进程。如果某个标签页失去响应,可以关掉这个标签页,此时其它标签页依然运行着,可以正常使用。如果所有标签页都运行在同一进程上,那么当某个失去响应,所有标签页都会失去响应,体验会很糟糕。如图:
把浏览器工作分成多个进程的另一好处是安全性与沙箱化。由于操作系统提供了限制进程权限的方法,浏览器可以用沙箱保护某些特定功能的进程。例如,Chrome 浏览器限制处理任意用户输入的进程(如渲染器进程)对任意文件的访问。
由于进程有自己的私有内存空间,所以它们通常包含公共基础设施的拷贝(如 V8,它是 Chrome 的 JavaScript 引擎)。这意味着使用了更多的内存,如果它们是同一进程中的线程,就无法共享这些拷贝。为了节省内存,Chrome 对可加速的内存数量进行了限制。具体限制数值依设备可提供的内存与 CPU 能力而定,但是当 Chrome 运行时达到限制时,会开始在同一站点的不同标签页上运行同一进程。
1-2、服务化
同样的方法也适用于浏览器进程。Chrome 正在经历架构变革,它转变为将浏览器程序的每一模块作为一个服务来运行,从而可以轻松实现进程的拆解或聚合。
通常观点是当 Chrome 运行在强力硬件上时,它会将每个服务分解到不同进程中,从而提升稳定性,但是如果 Chrome 运行在资源有限的设备上时,它会将服务聚合到一个进程中从而节省了内存占用。在这一架构变革实现前,类似的整合进程以减少内存使用的方法已经在 Android 类平台上使用。
1-3、站点隔离
站点隔离为每个 iframe 运行一个单独的渲染进程。每个标签页的渲染进程允许跨站点 iframe 运行在一个单独的渲染进程,在不同站点中共享内存。运行 a.com
与 b.com
在同一渲染进程中看起来还 OK。
同源策略 是 web 的核心安全模型。同源策略确保站点在未得到其它站点许可的情况下不能获取其数据。安全攻击的一个主要目标就是绕过同源策略。进程隔离是分离站点的最高效的手段。随着 Meltdown and Spectre 的出现,使用进程来分离站点愈发势在必行。Chrome 67 版本后,桌面版 Chrome 都默认开启了站点隔离,每个标签页的 iframe 都有一个单独的渲染进程。
启用站点隔离是多年来工程人员努力的结果。站点隔离并不只是分配不同的渲染进程这么简单。它从根本上改变了 iframe 的通信方式。在一个页面上打开开发者工具,让 iframe 在不同的进程上运行,这意味着开发者工具必须在幕后工作,以使它看起来无缝。即使运行一个简单的 Ctrl + F
来查找页面中的一个单词,也意味着在不同的渲染器进程中进行搜索。
2、浏览器内核的线程架构
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
2-1、GUI 渲染线程
- 主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等。
- 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
- 该线程与 JS 引擎线程互斥,当执行 JS 引擎线程时,GUI 渲染会被挂起,当任务队列空闲时,主线程才会去执行 GUI 渲染。
2-2、JS 引擎线程
- 该线程当然是主要负责处理 JS 脚本,执行代码。
- 也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS 引擎线程的执行。
- 当然,该线程与 GUI 渲染线程互斥,当 JS 引擎线程执行 JS 脚本时间过长,将导致页面渲染的阻塞。
为什么 JS 要是单线程的
这是因为 JS 这门脚本语言诞生的使命所致:JS 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果 JS 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突;如果 JS 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,JS 在最初就选择了单线程执行。
2-3、定时器触发线程
- 负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval。
- 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JS 引擎线程执行。
2-4、事件触发线程
- 主要负责将准备好的事件交给 JS 引擎线程执行。
比如 setTimeout 定时器计数结束,ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS 引擎线程的执行。
2-5、异步 http 请求线程
- 负责执行异步请求一类的函数的线程,如:Promise、axios、ajax 等。
- 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JS 引擎线程执行。
四、什么是 JavaScript 引擎
JavaScript 是一种解释型、基于对象的单线程脚本语言,JavaScript 引擎是一个专门处理 JavaScript 脚本的虚拟机,一般会附带在网页浏览器中。
换句话说,JavaScript 引擎能读懂 JS 代码,并准确地给出代码运行结果的一段程序。如 var a = 1 + 2;
JavaScript 引擎的作用就是(解析)这行代码,并且将 a 的值变为 3。
1、虚拟机
虚拟机(virtual machine)是指一种特殊的软件,可以在计算机平台和终端用户之间创建一种环境,而终端用户则基于这个软件所创建的环境来操作软件。
根据虚拟机的运用和直接机器的相关性分为两类:
- 系统虚拟机:提供一个可以运行完整操作系统的完整系统平台。
- 程序虚拟机:运行单个计算机程序设计,这意谓它支持单个进程。
2、编译器与解释器
编译器:将源代码编译为另外一种代码。
解释器:直接解析并将代码运行结果输出。
3、编译型语言与解释型语言
分类 | 说明 | 优点 | 缺点 |
---|---|---|---|
编译型语言 | 程序在执行前需要编译成机器语言的文件(.exe 文件),运行时直接用编译后的文件(.exe 文件)即可,不需要重新编译。 | 执行效率高 | 跨平台性差 |
解释型语言 | 程序不需要编译,在运行的过程中用解释器编译成机器语言,边编译边执行(没有 .exe 文件) | 跨平台性好 | 执行效率低 |
很难去界定说 JavaScript 引擎是解释器还是编译器,举个例子,V8(Chrome 的 JS 引擎)为了提高 JS 的运行性能,在运行前会先将 JS 编译为本地的机器码(native machine code),再执行机器码(这样速度就快很多)
4、为什么 JS 比 Java 慢
- JavaScript 变量无类型信息,不能做偏移信息查找,偏移信息共享等编译阶段的优化。
- JavaScript 将源码编译为字节码的过程要占用运行时间,而 Java 的编译则是开发阶段,不占用任何运行时间,因此 Java 可以尽可能的在编译阶段做优化。
五、JavaScript 引擎的组成
- 编译器(parser):将 JS 源码转成 AST(抽象语法树)
- 解释器(interperter):转换 AST(抽象语法树)成字节码并解释执行。
- JIT 工具:将字节码或 AST(抽象语法树)转换成机器码,之后可以直接执行机器码。
- 垃圾回收器(garbage collector):负责垃圾回收,清理堆内存中不再使用的对象。
推动 JavaScript 运行速度提高的一大利器是 JIT 技术,其作用是解决解释性语言的性能问题,主要思想是当解释器将源代码解释成内部表示的时候(类似于 Java 字节码),JavaScript 的执行环境不仅是解释这些内部表示,而且将其中一些字节码(使用率高的部分)转成本地代码(汇编代码),这样就可以被 CPU 直接执行,而不是解释执行,从而提高性能。
六、常见的 JavaScript 引擎
- V8 (Google):用 C++ 编写,开放源代码,Google (丹麦) 研发小组在 2006 年开始研发 V8。
- JavaScriptCore (Apple):开放源代码,用于 webkit 型浏览器(如 Safari),2008 年实现了编译器和字节码解释器,升级为 SquirrelFish。苹果内部代号为 Nitro 的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
- Rhino (Mozilla):开放源代码,用 Java 编写,用于 HTMLUnit。
- SpiderMonkey (Mozilla):第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于 Mozilla Firefox。
- Chakra (Microsoft):其 JScript 引擎用于 IE,其 JavaScript 引擎用于 Microsoft Edge。
关于 V8 引擎
V8 引擎的第一个版本随着 Chrome 于 2008 年发布,它的高性能被很多人青睐:
- Chrome 浏览器的 JS 引擎是 V8。
- Node.js 的运行时环境是 V8。
- electron 的底层引擎也是 V8。
在 V8 早期(5.9 版本以前)没有解释器,但有两个编译器,其编译流程如下:
- 解释器生成抽象语法树 AST。
- 编译器 Full-codegen 基准编译器直接生成机器码。
- 运行一段时间后,由分析器线程优化 JS 代码。
- 编译器 CrankShaft 优化编译器重新生成 AST 提升运行效率。
这样设计的缺点:
- 机器码会占用大量的内存。
- 缺少中间层机器码,无法实现一些优化策略。
- 无法很好的支持和优化 JS 的新语特性,无法拥抱未来。
于是新版本的 V8 在流程上进行了优化:
- 解析器生成 AST 抽象语法树。
- 解释器 Ignition 生成 byteCode 字节码并直接执行。
- 清除 AST 释放内存空间。
- 得到 25% - 50% 的等效机器代码大小。
- 编译器运行过程中,解释器收集优化信息发送给编译器 TurboFan。
- 重新生成机器码。
- 有些热点函数变更会由优化后的机器码还原成字节码,也就是 deoptimization 回退字节码操作执行。
其中的优化点:
- 值声明未调用,不会被解析生成 AST。
- 函数只被调用一次,bytcode 直接被解释执行,不会进入到编译优化阶段。
- 函数被调用多次,Igniton 会收集函数类型信息,可能会被标记为热点函数,可能被编译成优化后的机器代码。
好处:
- 由于一开始不需要直接编译成机器码,生成了中间层的字节码,从而节约了时间。
- 优化编译阶段,不需要从源码重新解析,直接通过字节码进行优化,也可以 deoptimization 回退操作。
七、JS 引擎与 ECMAScript 的关系
JavaScript 引擎是一段程序,我们写的 JavaScript 代码也是程序,如何让程序去读懂程序呢?
举个例子,var a = 1 + 1;
表示:
- 左边 var 代表声明了 a 这个变量。
- 右边的 + 表示要将 1 和 1 做加法。
- 中间的 = 表示这是个赋值语句。
- 最后的分号表示语句结束。
JavaScript 引擎根据上述这类规则去解析 JavaScript 代码,而 ECMAScript 就定义了这些规则。
换句话说,ECMAScript 定义了语言的标准,JavaScript 引擎根据它来实现,这就是两者的关系。
ECMAScript 262,就是对 JavaScript 这门语言定义了一整套完整的标准。
八、JS 引擎和渲染引擎的关系
网页的工作过程需要两个引擎,渲染引擎和 JavaScript 引擎。
JavaScript 引擎负责执行 JavaScript 代码,渲染引擎负责渲染网页。
JavaScript 引擎提供调用接口给渲染引擎,以便让渲染引擎使用 JavaScript 引擎来处理 JavaScript 代码并获取结果。
JavaScript 引擎需要能够访问渲染引擎构建的 DOM 树,所以 JavaScript 引擎通常需要提供桥接的接口,渲染引擎根据桥接接口来提供让 JavaScript 访问 DOM 的能力。
两种引擎通过桥接接口来操作 DOM 结构,造成了性能的损失。所以目前为止使用 JavaScript 操作 DOM 还是一个非常低效率的事。目前主流的解决方案是使用虚拟 DOM 的方式。