浏览器渲染
# 浏览器渲染
# 前置知识
# 进程和线程
进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
线程依赖于进程而存在,一个线程只能属于一个进程,一个进程可以有一个线程或者多个线程。
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量)
进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
进程间不会相互影响;而一个线程挂掉将导致整个进程挂掉。
进程适应于多核、多机分布;线程适用于多核
# 编译型语言和解释型语言
在程序中,有编译型语言和解释型语言。那么什么是编译型语言,什么是解释型语言呢?
编译型语言: 它首先将源代码编译成机器语言,再由机器运行机器码(二进制)。
解释型语言: 相对于编译型语言而存在的,源代码不是直接编译为目标代码,而是将源代码翻译成中间代码,再由解释器对中间代码进行解释运行的。 比如 javascript/python 等都是解释型语言(但是 javascript 是先编译完成后,再进行解释的)。
主要的编译型语言有 c++, 解释型语言有 Javascript, 和半解释半编译(比如 java)。
# 浏览器的结构
用户界面:包括地址栏,前进、后退按钮,书签菜单等。
浏览器引擎:在用户界面和呈现引擎之间传送指令。
呈现引擎:负责显示请求的内容。
网络:用于网络调用,比如 HTTP 请求;其接口与平台无关,并为所有平台提供底层实现。
用户界面后端:用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
JavaScript 解释器:用于解析和执行 JavaScript 代码。
数据存储:这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范定义了“网络数据库”,这是一个完整的浏览器内数据库。
注意:Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例,每个标签页都是一个独立的进程。
# 浏览器内核
Trident IE、猎豹安全、360 极速浏览器、百度浏览器
Gecko firefox 可惜这几年已经没落了,打开速度慢、升级频繁、猪一样的队友 flash、神一样的对手 chrome。
webkit Safari 从 Safari 推出之时起,它的渲染引擎就是 Webkit
Blink Chrome 和 Opera 2013 年,谷歌宣布从 WebKit 分支出来,创建了渲染引擎 Blink。
# 浏览器前缀
-webkit- (谷歌,Safari,新版 Opera 浏览器等)
-moz- (火狐浏览器)
-o- (旧版 Opera 浏览器等)
-ms- (IE 浏览器和 Edge 浏览器)
# 浏览器架构
浏览器架构其实大概可以分为两种架构,
一种是单进程架构,也就是只启动一个进程,这个进程里面有多个线程工作。
第二种是多进程架构,浏览器会启动多个进程,每个进程里面有多个线程,不同进程通过 IPC 进行通信。
# 浏览器的多进程
- 浏览器进程: 这是浏览器的主进程,负责浏览器界面的显示、各个页面的管理。每次我们打开浏览器,都会启动一个 Browser 进程,结束该进程就会关闭我们的浏览器。
你可以理解浏览器进程为一个统一的"调度大师"去调度其他进程,比如我们在地址栏输入 url 时,浏览器进程首先会调用网络进程。 它可以做一些子进程管理以及一些存储的处理。
渲染进程: 这是网页的渲染进程,负责页面的渲染工作,一般来说,一个页面都会对应一个 Renderer 进程,不过也有例外。
网络进程: 这个进程是控制对于一些静态资源的请求,它将资源请求完成之后会交给渲染进程进行渲染。
GPU 进程: 这个进程可以调用硬件进行渲染,从而实现渲染加速。比如 translate3d 等 css3 属性会骗取调用 GPU 进程从而开启硬件加速。
如果页面启动了硬件加速,浏览器就会开启一个 GPU 进程,但是最多只能有一个,当且仅当 GPU 硬件加速打开的时候才会被创建。
- 插件进程: chrome 中的插件也是一个独立的进程。
Chrome 为什么要使用多进程架构呢?
第一,更高的容错性。当今 WEB 应用中,HTML,JavaScript 和 CSS 日益复杂,这些跑在渲染引擎的代码,频繁的出现 BUG,而有些 BUG 会直接导致渲染引擎崩溃,多进程架构使得每一个渲染引擎运行在各自的进程中,相互之间不受影响,也就是说,当其中一个页面崩溃挂掉之后,其他页面还可以正常的运行不收影响。
第二,更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠
第三,更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺 CPU 资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。
Chrome 架构:仅仅打开了 1 个页面,为什么有 4 个进程?
浏览器(Browser)主进程、GPU 进程、网络(NetWork)进程、渲染进程和插件进程。
打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
打开 2 个页面会有 5 个进程,这五个进程分别是:1 个浏览器主进程、1 个 GPU 进程、1 个网络进程和 2 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
特殊情况:
- 如果页面中有插件,插件也需要一个单独的进程。
- 如果页面中有 iframe,iframe 也会运行在单独的进程中。
- 如果你装了扩展,扩展也会占用进程。
- 如果两个页面属于同一个站点,并且 B 页面是从 A 页面中打开的,那么他们会共用一个渲染进程。
可以通过任务管理器来更简单更直观的查看浏览器到底打开了几个进程。
多进程架构优化
之前的我们说到,Renderer Process 的作用是负责一个 Tab 内的显示相关的工作,这就意味着,一个 Tab,就会有一个 Renderer Process,这些进程之间的内存无法进行共享,而不同进程的内存常常需要包含相同的内容。
# 浏览器的进程模式
为了节省内存,Chrome 提供了四种进程模式(Process Models),不同的进程模式会对 tab 进程做不同的处理。
Process-per-site-instance (default) - 同一个 site-instance 使用一个进程
Process-per-site - 同一个 site 使用一个进程
Process-per-tab - 每个 tab 使用一个进程
Single process - 所有 tab 共用一个进程
site 指的是相同的 registered domain name(如: google.com ,bbc.co.uk)和 scheme (如:https://)。比如 a.baidu.com 和 b.baidu.com 就可以理解为同一个 site(注意这里要和 Same-origin policy 区分开来,同源策略还涉及到子域名和端口)。
site-instance 指的是一组 connected pages from the same site,这里 connected 的定义是 can obtain references to each other in script code 怎么理解这段话呢。满足下面两中情况并且打开的新页面和旧页面属于上面定义的同一个 site,就属于同一个 site-instance
(1) 用户通过<a target="_blank">
这种方式点击打开的新页面
(2) JS 代码打开的新页面(比如 window.open
)
理解了概念之后,下面解释四个进程模式
首先是 Single process,顾名思义,单进程模式,所有 tab 都会使用同一个进程。接下来是 Process-per-tab ,也是顾名思义,每打开一个 tab,会新建一个进程。而对于 Process-per-site,当你打开 a.baidu.com 页面,在打开 b.baidu.com 的页面,这两个页面的 tab 使用的是共一个进程,因为这两个页面的 site 相同,而如此一来,如果其中一个 tab 崩溃了,而另一个 tab 也会崩溃。
Process-per-site-instance 是最重要的,因为这个是 Chrome 默认使用的模式,也就是几乎所有的用户都在用的模式。当你打开一个 tab 访问 a.baidu.com ,然后再打开一个 tab 访问 b.baidu.com,这两个 tab 会使用两个进程。而如果你在 a.baidu.com 中,通过 JS 代码打开了 b.baidu.com 页面,这两个 tab 会使用同一个进程。
默认模式选择
那么为什么浏览器使用 Process-per-site-instance 作为默认的进程模式呢?
Process-per-site-instance 兼容了性能与易用性,是一个比较中庸通用的模式。
相较于 Process-per-tab,能够少开很多进程,就意味着更少的内存占用
相较于 Process-per-site,能够更好的隔离相同域名下毫无关联的 tab,更加安全
# 浏览器的渲染进程是多线程的
- GUI 渲染线程
负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
- JS 引擎线程
也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)
JS 引擎线程负责解析 Javascript 脚本,运行代码。
JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序
同样注意,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 事件触发线程
归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助)
当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
注意,由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)
- 定时触发器线程
传说中的 setInterval 与 setTimeout 所在线程
浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行)
注意,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。
- 异步 http 请求线程
在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。
梳理浏览器内核中线程之间的关系
- GUI 渲染线程与 JS 引擎线程互斥
由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JS 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JS 引擎为互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起, GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行。
# 导航过程中的网页加载过程
深入了解进程和线程是如何呈现我们的网站页面
tab 以外的大部分工作由浏览器进程 Browser Process 负责,针对工作的不同,Browser Process 划分出不同的工作线程:
UI thread:控制浏览器上的按钮及输入框;
network thread:处理网络请求,从网上获取数据;
storage thread: 控制文件等的访问;
# 第一步:处理输入
当我们在浏览器的地址栏输入内容按下回车时,UI thread 会判断输入的内容是搜索关键词(search query)还是 URL,如果是搜索关键词,跳转至默认搜索引擎对应都搜索 URL,如果输入的内容是 URL,则开始请求 URL。
# 第二步:开始导航
回车按下后,UI thread 将关键词搜索对应的 URL 或输入的 URL 交给网络线程 Network thread,此时 UI 线程使 Tab 前的图标展示为加载中状态,然后网络进程进行一系列诸如 DNS 寻址,建立 TLS 连接等操作进行资源请求,如果收到服务器的 301 重定向响应,它就会告知 UI 线程进行重定向然后它会再次发起一个新的网络请求。
# 第三步:读取响应
network thread 接收到服务器的响应后,开始解析 HTTP 响应报文,然后根据响应头中的 Content-Type 字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个 HTML 文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
与此同时,浏览器会进行 Safe Browsing 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。
# 第四步:查找渲染进程
各种检查完毕以后,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。
浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。
# 第五步:提交导航
到了这一步,数据和渲染进程都准备好了,Browser Process 会向 Renderer Process 发送 IPC 消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送 IPC 消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。
这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。
# 第六步:初始化加载完成
当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的 iframe 都触发了 onload 事件),会向浏览器进程发送 IPC 消息,告知浏览器进程,这个时候 UI thread 会停止展示 tab 中的加载中图标。
# 渲染引擎简介
# 渲染主流程
浏览器从接收到页面开始到页面显示,这整个过程中的所有步骤,称 关键渲染路径 ,页面内容加载完成和页面资源完成,分别对应于 DOMContentLoaded 和 Load
网页的渲染过程包含页面加载和页面渲染两个过程。
页面加载过程是,从服务器请求资源并构建 DOM 树的过程
网页渲染过程指的是通过 DOM 树渲染出视图内容。
解析 CSSOM 树 和 DOM 树 是并行的
- 解析 HTML,生成 DOM 树(和第二步并行)
过程:字节->字符->语义块->节点->DOM (Bytes → characters → tokens → nodes → object model).
DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
- 解析 CSS,生成 CSSOM 树(和第一步并行)
过程:字节->字符->语义块->节点->DOM
解析 CSS 样式,解析的顺序是浏览器的样式 -> 用户自定义的样式 -> 页面的 link 标签等引进来的样式 -> 写在 style 标签里面的内联样式
- 将 DOM 树 和 CSSOM 树 合并,生成渲染树(Rendere-Tree)
(1)、过滤掉不可见节点(脚本标记、元标记)
(2)、过滤掉样式隐藏的节点(display:none)
- 计算渲染树的布局 Layout
根据渲染树来布局,计算节点的几何信息(layout)
- 将布局渲染到屏幕上 Paint
将各个节点绘制在屏幕上(paint)
# 浏览器加载、解析、渲染过程
- 加载:
了解浏览器如何进行加载,可以在引用外部样式文件,外部 js 时,将他们放到合适的位置,使浏览器以最快的速度将文件加载完毕。
- 解析:
了解浏览器如何进行解析,我们可以在构建 DOM 结构,组织 css 选择器时,选择最优的写法,提高浏览器的解析速率。
- 渲染:
了解浏览人如何进行渲染,我们可以在设置元素属性,编写 js 文件时,可以减少"重排""重绘"的消耗。
总结:这三个过程在事件进行的时候不是完全独立,会有交叉。会一边加载,一遍解析,一遍渲染的工作想象。
# 正常网页加载的顺序
html 是从上往下加载
浏览器一边下载 HTML 网页,一边开始解析
解析过程中,发现
<script>
标签暂停解析,网页渲染的控制权转交给 JavaScript 引擎如果
<script>
标签引用了外部脚本,就下载该脚本,否则就直接执行执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页
外链的 js 如果含有 defer="true"属性,将会并行加载 js,到页面全部加载完成后才会执行,会按顺序执行。
外链的 js 如果含有 async="true"属性,将不会依赖于任何 js 和 css 的执行,此 js 下载完成后立刻执行,不保证按照书写的顺序执行。因为 async="true"属性会告诉浏览器,js 不会修改 dom 和样式,故不必依赖其它的 js 和 css。
# DOMContentLoaded vs load
(1) DOMContentLoaded 是指页面元素加载完毕,但是一些资源比如图片还无法看到,但是这个时候页面是可以正常交互的,比如滚动,输入字符等。 jQuery 中经常使用的 $(document).ready() 其实监听的就是 DOMContentLoaded 事件。
(2) load 是指页面上所有的资源(图片,音频,视频等)加载完成。jQuery 中 $(document).load() 监听的是 load 事件。
# css 加载、解析
css 异步加载(不阻塞)
css 的解析是从右往左逆向解析的(从 DOM 树的下-上解析比上-下解析效率高),嵌套标签越多,解析越慢。
css 的加载和解析不会阻塞 HTML 的解析(即:不会阻塞 Dom 树的生成),不会阻塞其它资源(如图片)的加载,
css 的加载和解析会阻塞 渲染树 RenderTree 的生成,也会阻塞界面的渲染!
css 加载会阻塞后面 js 语句的执行
渲染树 RenderTree 是根据 Dom 和 CSSOM 生成的,所以,在 CSSOM 还未完成之前, 是无法生成渲染树的,之后的流程更是无法进行;所以 CSS 的加载和解析会阻塞 生成渲染树 RenderTre 及其之后的流程;
css 的位置会影响页面效率,为什么?
css 在加载过程中不会影响到 DOM 树的生成,但是会影响到 Render 树的生成,进而影响到 layout,所以一般来说,style 的 link 标签需要尽量放在 head 里面,因为在解析 DOM 树的时候是自上而下的,而 css 样式又是通过异步加载的,这样的话,解析 DOM 树下的 body 节点和加载 css 样式能尽可能的并行,加快 Render 树的生成的速度。
# js 加载、解析
js 同步加载(阻塞)
没有设置 defer 或 async 的 js 的加载会阻塞 HTML 的解析(即:会阻塞 Dom 树的生成),设置了 defer 或 async 的 js 不阻塞 HTML 的解析;
JS 的执行,都会阻塞 HTML 的解析
JS 的位置会影响页面效率,为什么?
js 脚本应该放在底部,原因在于 js 线程与 GUI 渲染线程是互斥的关系,如果 js 放在首部,当下载执行 js 的时候,会影响渲染行程绘制页面,js 的作用主要是处理交互,而交互必须得先让页面呈现才能进行,所以为了保证用户体验,尽量让页面先绘制出来。
JS 和 CSS 在页面中的位置,会影响其他资源(指 img 等非 js 和 css 资源)的加载顺序,究其原因,有三个值得注意的点:
JS 有可能会修改 DOM. 典型的,可能会有 document.write. 这意味着,在当前 JS 加载和执行完成前,后续所有资源的下载有可能是没必要的。这是 JS 阻塞后续资源下载的根本原因。
JS 的执行有可能依赖最新样式。比如,可能会有 var width = $('#id').width(). 这意味着,JS 代码在执行前,浏览器必须保证在此 JS 之前的所有 css(无论外链还是内嵌)都已下载和解析完成。这是 CSS 阻塞后续 JS 执行的根本原因。
现代浏览器很聪明,会进行 prefetch 优化。性能是如此重要,现代浏览器在竞争中,在 UI update 线程之外,还会开启另一个线程,对后续 JS 和 CSS 提前下载(注意,仅提前下载,并不执行)。有了 prefetch 优化,这意味着,在不存在任何阻塞的情况下,理论上 JS 和 CSS 的下载时机都非常优先,和位置无关。
script 标签的位置会影响首屏时间么?
如果 script 标签的位置不在首屏范围内,不影响首屏时间 (如果这里里的首屏指的是页面从白板变成网页画面——也就是第一次 Painting),但有可能截断首屏的内容,使其只显示上面一部分。
什么场景下 js 会放在 body 的首部而不是尾部?
个别特殊 JS,比如用于调试的基础脚本(部署时未必有)、性能日志之类,必须放在尽量最前的位置。
# js 执行
JavaScript 代码的执行过程大致可以分为语法检查和运行两个阶段
语法检查包括词法分析和语法分析
- 词法分析,会将字符组成的字符串分解为有意义的代码块。这些代码块被称为词法单元。
例如:对于程序 var a = 2;,会被分解为 var a = 2 ;
- 语法分析,会将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。
这个树被称为`抽象语法树(Abstract Syntax Tree,AST)
运行阶段包括预编译和执行
预编译,将生成 AST 复制到当前执行的上下文中。对当前 AST 中的变量声明、函数声明及函数形参进行属性填充。
JavaScript 逐行读取并运行代码
# defer 和 async 属性的区别
script 标签存在两个属性,defer 和 async
- async 属性
顺序:加载优先顺序,脚本在文档中的顺序不重要,先加载完成先执行
表示后续文档的加载和渲染与 js 脚本的加载和执行是并行进行的,即异步执行
- defer 属性
顺序:文档顺序,
加载后续文档的过程和 js 脚本的加载(此时仅加载不执行)是并行进行的(异步),js 脚本的执行需要等到文档所有元素解析完成之后,DOMContentLoaded 事件触发执行之前
# 媒体资源加载、解析
媒体资源(如:图片音视频等)的加载不会阻塞 HTML 的解析!
媒体资源是并行加载的!
因为媒体资源的加载不会阻塞 HTML 的解析,那么,浏览器加载第一个媒体资源时,HTML 还可以继续往下解析,当解析到其它媒体资源的标签时,浏览器还可以继续加载相应的媒体资源,所以媒体资源是并行加载的!
# 浏览器渲染问题汇总
# JavaScript 为什么会阻塞渲染?css 会不会阻塞渲染
js 在加载之前,浏览器不能识别 js 具体的操作,js 是可以操作 DOM 的,如果在修改元素属性的同时渲染界面,会出现不可预期的结果,导致渲染混乱,因此浏览器设置了 GUI 渲染现成和 js 线程互斥,在 js 执行的时候,GUI 线程会被挂起,等空闲的时候再执行。
dom 解析和 css 解析是两个并行的线程,所以 css 加载不会阻塞 dom 的解析
但是 render tree 是依赖 dom tree 和 cssom tree 的,因此,css 的加载会阻塞 dom 的渲染
# 白屏
前端优化-如何计算白屏和首屏时间 https://www.cnblogs.com/longm/p/7382163.html (opens new window)
# 白屏原因
网络原因:页面渲染被阻塞,在弱网络下(2G 网路或者 GPRS 网络) ,网络延迟,JS 加载延迟 ,会阻塞页面
前端代码错误:客户端存在 bug,js 报错或者语法不兼容
# 白屏解决方案
骨架屏
预渲染 prerender-spa-plugin
服务器端渲染
# 注意:网络原因要给用户反馈
参考:
精读 - 浏览器渲染原理 https://segmentfault.com/a/1190000022697385 (opens new window)
从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理 https://segmentfault.com/a/1190000012925872 (opens new window)