写在前面
进程、线程、内存、词法语法解析都了解过,但是作为一个前端工程师,浏览器对我们来说更像一个黑盒。比如页面间怎么通信的?扩展和页面怎么通信的?dom css js 是怎么被浏览器解析的?普通 js 下载运行为什么会阻塞 dom 的解析?
浏览器每个小细节的实现都包含了很多的内容,本文没有覆盖所有问题,也可能没有把每个问题讲解的足够深入细致。但希望可以让你对浏览器本身的运行机制有一些好奇心~
声明
本文所有观点均基于桌面 chrome 浏览器。
一、为什么是多进程架构
活动监视器截图由图可以看到,浏览器是多进程的。那我们探索下为什么浏览器是多进程的呢?
二、浏览器多进程架构
多进程架构图一般,当 Chrome 在强大的硬件上运行时,它可能会将每个服务拆分为不同的进程,从而提供更高的稳定性,但如果它位于资源约束设备上,Chrome会将部分服务整合到一个进程中,从而节省内存占用。 下面介绍下主要的进程。
三、主要进程有哪些?
Browser 进程:浏览器的主进程,负责浏览器的界面显示、各个页面的管理,是所有其他类型进程的祖先,负责他们的创建和销毁调用等工作,它有且仅有一个。
Renderer进程:网页的渲染进程,负责页面的渲染工作主要在这个进程中完成,会有多个。
Until very recently, Chrome gave each tab a process when it could; now it tries to give each site its own process, including iframes (seeSite Isolation).
when it could 是什么意思呢?
In order to save memory, Chrome puts a limit on how many processes it can spin up. The limit varies depending on how much memory and CPU power your device has, but when Chrome hits the limit, it starts to run multiple tabs from the same site in one process.
插件进程:其创建的基本原则是每种类型的插件只创建一次,而且仅当使用时才创建。当多个网页需要使用同一种类型的插件时,插件进程会为每个使用者创建一个实例,所以插件进程是被共享的。
GPU进程:最多只有一个,当且仅当 GPU 硬件加速打开的时候才会被创建,主要用于对 3D 图形加速调用的实现
其他进程
四、多进程的优点
1、browser 和 render 是分开的,避免单个页面render crash 影响整个浏览器
(https://developers.google.com/web/updates/images/inside-browser/part1/tabs.svg)
2、它方便了安全机制的实现,沙箱模型是基于多进程架构的
对于网络上的网页,浏览器认为它们是不安全的,因为网页总是存在各种可能性,也许是无意的或有意的攻击。如果有一种机制,将网页的运行限制在一个特定的环境中,也就是一个沙箱中,使它只能访问有限的功能。那么,即使网页工作的渲染引擎被攻击,它也不能够获取渲染引擎工作的主机系统中的任何权限,这一思想就是沙箱模型。
五、当一个 url 输入时,哪些进程被调用
我们知道浏览器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又对这些工作进一步划分,使用不同线程进行处理:
UI thread : 控制浏览器上的按钮及输入框;
network thread: 处理网络请求,从网上获取数据;
storage thread: 控制文件等的访问;
浏览器主进程中的不同线程回到我们的问题,当我们在浏览器地址栏中输入文字,并点击回车获得页面内容的过程在浏览器看来可以分为以下几步:
1. 处理输入
UI thread 需要判断用户输入的是 URL 还是 query;
2. 开始导航
当用户点击回车键,UI thread 通知 network thread 获取网页内容,并控制 tab 上的 spinner 展现,表示正在加载中。
network thread 会执行 DNS 查询,随后为请求建立 TLS 连接。
UI thread 通知 Network thread 加载相关信息
如果 network thread 接收到了重定向请求头如 301,network thread 会通知 UI thread 服务器要求重定向,之后,另外一个 URL 请求会被触发。
3. 读取响应
当请求响应返回的时候,network thread 会依据 Content-Type 及 MIME Type sniffing 判断响应内容的格式。
判断响应内容的格式
如果响应内容的格式是 HTML ,下一步将会把这些数据传递给 renderer process,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
Safe Browsing 检查也会在此时触发,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。此外 CORB 检测也会触发确保敏感数据不会被传递给渲染进程。
4. 查找渲染进程
当上述所有检查完成,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。
收到 Network thread 返回的数据后,UI thread 查找相关的渲染进程
由于网络请求获取响应需要时间,这里其实还存在着一个加速方案。当 UI thread 发送 URL 请求给 network thread 时,浏览器其实已经知道了将要导航到那个站点。UI thread 会并行的预先查找和启动一个渲染进程,如果一切正常,当 network thread 接收到数据时,渲染进程已经准备就绪了,但是如果遇到重定向,准备好的渲染进程也许就不可用了,这时候就需要重启一个新的渲染进程。
5. 确认导航
进过了上述过程,数据以及渲染进程都可用了, Browser Process 会给 renderer process 发送 IPC 消息来确认导航,一旦 Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载过程开始。
此时,地址栏会更新,展示出新页面的网页信息。history tab 会更新,可通过返回键返回导航来的页面,为了让关闭 tab 或者窗口后便于恢复,这些信息会存放在硬盘中。
6. 额外的步骤
一旦导航被确认,renderer process 会使用相关的资源渲染页面,下文中我们将重点介绍渲染流程。当 renderer process 渲染结束(渲染结束意味着该页面内的所有的页面,包括所有 iframe 都触发了 onload 时),会发送 IPC 信号到 Browser process, UI thread 会停止展示 tab 中的 spinner。
Renderer Process 发送 IPC 消息通知 browser process 页面已经加载完成。
当然上面的流程只是网页首帧渲染完成,在此之后,客户端依旧可下载额外的资源渲染出新的视图。
在这里我们可以明确一点,所有的 JS 代码其实都由 renderer Process 控制的,所以在你浏览网页内容的过程大部分时候不会涉及到其它的进程。不过也许你也曾经监听过 beforeunload 事件,这个事件再次涉及到 Browser Process 和 renderer Process 的交互,当当前页面关闭时(关闭 Tab ,刷新等等),Browser Process 需要通知 renderer Process 进行相关的检查,对相关事件进行处理。
浏览器进程发送 IPC 消息给渲染进程,通知要离开当前网站了
如果导航由 renderer process 触发(比如在用户点击某链接,或者 JS 执行 window.location = "http://newsite.com" ) renderer process 会首先检查是否有 beforeunload 事件处理器,导航请求由 renderer process 传递给 Browser process。
如果导航到新的网站,会启用一个新的 render process 来处理新页面的渲染,老的进程会留下来处理类似 unload 等事件。
关于页面的生命周期,更多内容可参考 Page Lifecycle API 。
浏览器进程发送 IPC 消息到新的渲染进程通知渲染新的页面,同时通知旧的渲染进程卸载。
除了上述流程,有些页面还拥有 Service Worker (服务工作线程),Service Worker 让开发者对本地缓存及判断何时从网络上获取信息有了更多的控制权,如果 Service Worker 被设置为从本地 cache 中加载数据,那么就没有必要从网上获取更多数据了。
值得注意的是 service worker 也是运行在渲染进程中的 JS 代码,因此对于拥有 Service Worker 的页面,上述流程有些许的不同。
当有 Service Worker 被注册时,其作用域会被保存,当有导航时,network thread 会在注册过的 Service Worker 的作用域中检查相关域名,如果存在对应的 Service worker,UI thread 会找到一个 renderer process 来处理相关代码,Service Worker 可能会从 cache 中加载数据,从而终止对网络的请求,也可能从网上请求新的数据。
Service Worker 依据具体情形做处理。
关于 Service Worker 的更多内容可参考:
https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
如果 Service Worker 最终决定通过网上获取数据,Browser 进程 和 renderer 进程的交互其实会延后数据的请求时间 。Navigation Preload 是一种与 Service Worker 并行的加速加载资源的机制,服务端通过请求头可以识别这类请求,而做出相应的处理。
更多内容可参考:
https://developers.google.com/web/updates/2017/02/navigation-preload
六、1、那么进程间怎么进行通信?2、扩展与页面间的通信?3、hybrid app 中 native 与 web 间的通信?4、页面间的通信?
1、Inter Process Communication (IPC)
2、chrome.runtime.onMessage.addListener && chrome.tabs.sendMessage
由于Extension运行时需要调用“chrome.*”接口,我们必须了解这些接口是如何被扩展和实现的。 从基本过程上来看,简单地讲应该是Chromium的Extension机制在V8引擎中注入一些代码,然后当JavaScript代码调用这些接口的时候,V8引擎调用注入的本地代码,这些代码会将调用接口的请求从Renderer进程发送给Browser进程。在Browser进程中,接收这些请求并派发给相应的实现类,请求完成后按需要返回调用结果。
3、js 与 java 通过对象注入(类似的js 与 oc window.webkit.messageHandlers )
网页开发者使用 html、css、js开发网页时,有时会觉得浏览器能力不足,希望通过传统语言例如 c/c++ 来开发一些库,这些库可以被网页调用,这样来满足应用的要求,这里称之为混合编程。
V8 提供什么机制来扩展 js引擎的能力的呢?1、idl 文件 2、扩展 extension 基类
4、window.postMessage()
七、渲染进程(浏览器内核)
Renderer process with a main thread, worker threads, a compositor thread, and a raster thread inside渲染进程包含的线程
1. 主线程 Main thread
2. 工作线程 Worker thread
3. 排版线程 Compositor thread
4. 光栅线程 Raster thread
八、渲染的流程
渲染流程主线程解析 dom 构建 dom 树——》preload scanner 页面中资源请求传递给 Browser process 中的 network thread 进行相关资源的下载——》JS、CSS 的下载与执行(注意思考各种阻塞的原因)——》主进程还会基于 CSS 选择器解析 CSS 获取每一个节点的最终的计算样式值。——》layout 通过遍历 DOM 及相关元素的计算样式,主线程会构建出包含每个元素的坐标信息及盒子大小的布局树。——》在绘制阶段,主线程会遍历布局树以创建绘制记录。绘制记录可以看做是记录各元素绘制先后顺序的笔记。——》主线程遍历布局树生成层树——》一旦层树被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程会栅格化每一层。有的层的可以达到整个页面的大小,因此,合成器线程将它们分成多个磁贴,并将每个磁贴发送到栅格线程,栅格线程会栅格化每一个磁贴并存储在 GPU 显存中。
九、小碎片
1、render 进程中 worker thread
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法,可以用于处理大型计算。只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享
Service Worker 在后台线程中运行的线程。是一个可编程的网络代理,允许开发者控制页面上处理的网络请求。
2、SharedWorker 和上面的 worker 是一回事么?
sharedWorker 是由浏览器管理的进程,可以被多个 render 进程共享。不管 sharedWorker 被创建多少次,每个浏览器只存在一个 sharedworker 进程。
3、css 加载不会阻塞 dom 树解析,但是会阻塞 dom 树渲染
浏览器是解析 DOM 生成 DOM Tree,结合 CSS 生成的 CSS Tree,最终组成 render tree,再渲染页面。可见 css 并不影响 dom tree 的生成。但是到了渲染阶段,渲染是有成本的,浏览器会尽量减少渲染的次数。
4、js 阻塞 DOM 解析和渲染(此处忽略 defer async)
js 会影响 dom tree 的生成,如果不阻塞解析,很可能之前的解析工作都是一些无用功,包括解析后的很多工作都是无用功,所以此处的阻塞也是出于对浏览器性能的优化。
5、reflow 与 repaint 相差的主要是哪个过程?
layout
6、event loop 相关的线程?
a、JS引擎线程
也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)JS引擎线程负责解析Javascript脚本,运行代码。JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
b、事件触发线程
归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
c、定时触发器线程
传说中的setInterval与setTimeout所在线程
浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
d、异步http请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
参考文档
https://developers.google.com/web/updates/2018/09/inside-browser-part1 (官方文档)
https://www.infoq.cn/article/CS9-WZQlNR5h05HHDo1b (对官方文档的部分翻译)
https://segmentfault.com/a/1190000012925872
https://www.imweb.io/topic/58e3bfa845e5c13468f567d5
https://juejin.im/post/5a6547d0f265da3e283a1df7
网友评论