前言
Chrome comic
,一本Chrome架构简要概述的漫画,Chrome架构于2008年同Chrome浏览器一起公布。
只管Chrome公布了十多年了,【Chrome comic】漫画中介绍的外围原理依然有助于了解Electron。(原文,中文)
漫画目录如下:
- 开源浏览器背地的故事
- 稳定性、严格和多任务架构
- 速度:Webkit和V8
- 搜寻和用户体验
- 安全性、沙盒模式和没有危险的浏览
- Gears,规范和凋谢源代码
一、CPU,GPU内存和多过程架构
计算机的外围是 CPU 和 GPU
CPU
CPU是计算机的大脑,能够解决许多不同的工作,大多数CPU都是单芯片。一个内核相当于同一个芯片中的另一个CPU。
GPU
GPU最后为图形处理开发,善于解决简略的工作,同时跨多个CPU。
通常,应用程序应用操作系统提供的机制在 CPU
和 GPU
上运行。
过程和线程
过程能够被形容为一个应用程序的执行程序,线程是存在于过程外部并执行其过程程序的任何局部的线程。
程序在启动的时候会创立一个过程,程序也可能会创立线程来帮忙它工作。操作系统为过程提供了一块“内存块”以供应用,并且所有应用程序状态都保留在该公有内存空间中。当敞开应用程序时,该过程也会隐没,操作系统会开释内存。
一个过程能够要求操作系统启动另一个过程来运行不同的工作,这时候会为新过程分配内存的不同局部。如果两个过程须要聊天,它们就须要 IPC
。如果工作过程无响应,它能够重新启动而无需进行运行应用程序不同局部的其余过程。
浏览器架构
对于浏览器,能够使一个过程有许多不同的线程,也能够是许多不同的过程有多个线程通过 IPC
通信的。
而对于Chrome浏览器,最新架构如下图:
过程 | 作用 |
---|---|
Browser | 浏览器过程,控制应用程序的“chrome”局部,包含地址栏、书签、后退和后退按钮。 还解决 Web 浏览器的不可见的特权局部,例如网络申请和文件拜访。 |
Renderer | 渲染器过程,管制显示网站的选项卡内的任何内容。 |
Plugin | 插件过程,管制网站应用的任何插件,例如 flash。 |
GPU | 图形处理过程,独立于其余过程解决 GPU 工作。它被分成不同的过程,因为 GPU 解决来自多个应用程序的申请并将它们绘制在同一个外表上。 |
下图为不同过程指向浏览器UI的不同局部:
当然也还有更多的过程,比方扩大过程和实用程序过程等等。
Chrome中多过程的劣势
假如,关上了三个选项卡,每个选项卡都由一个独立的渲染器过程运行。如果一个选项卡变得无响应,那能够敞开无响应的选项卡并持续操作,同时放弃其余选项卡的流动。如果所有选项卡都在一个过程上运行,当一个选项卡变得无响应时,所有选项卡都无响应。
将浏览器的工作分成多个过程的另一个益处是安全性和沙箱。因为操作系统提供了一种办法来限度过程的权限,浏览器能够从某些性能通过沙箱来执行某些过程。例如,Chrome 浏览器限度对解决任意用户输出的过程(如渲染器过程)的任意文件拜访。
因为过程有本人的公有内存空间,所以它们通常蕴含公共基础设施的正本(比方 V8,它是 Chrome 的 JavaScript 引擎)。这意味着更多的内存使用量,因为如果它们是同一过程内的线程,则无奈像它们那样共享它们。为了节俭内存,Chrome 限度了它能够启动的过程数。该限度取决于设施的内存和 CPU 能力,但当 Chrome 达到限度时,它会开始在一个过程中运行来自同一站点的多个选项卡。
节俭更多的内存 - Chrome 中的服务化
Chrome 正在经验架构更改,以将浏览器程序的每个局部作为一项服务运行,从而能够轻松地拆分为不同的过程或聚合为一个过程。
当 Chrome 在弱小的硬件上运行时,它可能会将每个服务拆分为不同的过程以提供更高的稳定性,但如果它在资源受限的设施上,Chrome 会将服务合并到一个过程中以节俭内存占用。在此更改之前,已在 Android 等平台上应用了相似的办法来合并过程以缩小内存使用量。
站点隔离
站点隔离为每个跨站点 iframe 运行独自的渲染器过程,并在不同站点之间共享内存空间。同源策略是网络的外围平安模型,它确保一个站点在未经批准的状况下无法访问其余站点的数据。对于攻击者来说,绕过同源策略是平安攻打的次要指标,对于浏览器而言,须要应用过程来分隔站点。自 Chrome 67 以来,桌面上默认启用站点隔离,选项卡中的每个跨站点 iframe 都有一个独自的渲染器过程, 当然,也从根本上扭转了 iframe 互相通信的形式。
二、导航跳转
在浏览器中写了一个URL,而后浏览器从 Internet 获取数据并显示一个页面,对于申请站点和浏览器渲染前都做了什么?
后面咱们晓得,选项卡之外的所有内容都由浏览器过程解决,也就是 Browser Process
。浏览器里的过程里有一些线程,比方绘制 Button
和 Input
的 UI 线程、解决网络堆栈以从 Internet 接收数据的网络线程、管制对文件拜访的存储线程等等。在地址栏中输出 URL 时,输出由浏览器过程的 UI 线程解决。
开始
第一步:解决输出
当地址栏中输出内容时,UI 线程首先询问的是“这是搜寻查问还是 URL?”。在 Chrome 中,地址栏也是一个搜寻输出字段,因而 UI 线程须要解析并决定是将它发送到搜索引擎,还是发送到申请的站点。
第二步:开始寻找
按下回车键时,UI 线程会发动网络申请以获取站点内容。Loading spinner 显示在选项卡的一角,网络线程通过适当的协定,如 DNS 查找和为申请建设 TLS 连贯。
此时,网络线程可能会收到服务器重定向标头,如 HTTP 301。在这种状况下,网络线程会与服务器申请重定向的 UI 线程通信。而后,将发动另一个 URL 申请。
第三步: 读取响应
一旦响应的开始进入,也就是申请的 Payload,网络线程会在必要时查看流的前几个字节。响应的 Content-Type 标头应该阐明它是什么类型的数据,但因为它可能失落或谬误, 因而在这里实现
MIME 类型校验
。如果响应是一个 HTML 文件,那么下一步是将数据传递给 GPU 过程,但如果它是一个 zip 文件或其余一些文件,那么它就是一个下载申请,接着他们须要将数据传递给下载管理器。
也正是在这个中央进行平安浏览查看,如果域和相应数据跟歹意网站匹配,网络线程就会收回警报并显示正告页面,而 CORS查看 也产生在这个过程,为了确保敏感跨站点数据不扔给渲染器。
第四步: 查找渲染器过程
一旦实现所有查看并且网络线程确信浏览器应该导航到申请的站点,网络线程就会通知 UI 线程数据已准备就绪。UI线程而后找到一个渲染器过程来进行网页的渲染。
因为网络申请可能须要数百毫秒能力取得响应,因而利用了优化以放慢此过程。当 UI 线程在第 2 步向网络线程发送 URL 申请时,它曾经晓得他们要导航到哪个站点。UI 线程尝试与网络申请并行地被动查找或启动渲染器过程。这样,如果所有按预期进行,当网络线程接管到数据时,渲染器过程曾经处于待机状态。如果导航重定向跨站点,则可能不会应用此备用过程,在这种状况下,可能须要不同的过程。
第五步:提交
当初数据和渲染器过程曾经准备就绪,一个 IPC 从浏览器过程发送到渲染器过程以提交导航。它还传递数据流,因而渲染器过程能够持续接管 HTML 数据。一旦浏览器过程听到在渲染器过程中产生提交的确认,导航就实现了,文档渲染阶段开始。
此时,地址栏已更新,平安指示器和站点设置 UI 反映了新页面的站点信息。选项卡的会话历史将更新,因而后退/后退按钮将逐渐浏览刚刚导航到的站点。为了在敞开选项卡或窗口时促成选项卡/会话复原,会话历史记录存储在磁盘上。
其余步骤
提交后,渲染器过程会持续加载资源并渲染页面。渲染器过程“实现”渲染后,它会将 IPC 发送回浏览器过程(这是在
onload
页面中的所有帧上触发所有事件并实现执行之后)。此时,UI 线程进行选项卡上的 加载小loading。在此之后客户端 JavaScript 依然能够加载额定的资源并出现新的视图。
导航到其余站点
如果用户再次将不同的 URL 放入地址栏会产生什么?浏览器过程通过雷同的步骤导航到不同的站点。但在此之前,它须要查看以后出现的站点是否有 beforeunload
事件。
beforeunload
能够创立一个 “来到此站点?” 的事件, 当来到或敞开选项卡时收回警报。选项卡内的所有内容(包含 JavaScript 代码)都由渲染器过程解决,因而当新的导航申请传入时,浏览器过程必须查看以后的渲染器过程。
留神:不要增加无条件beforeunload
处理程序。它会产生更多的提早,因为须要在导航开始之前执行处理程序。仅在须要时才应增加此事件处理程序,例如,如果须要正告用户他们可能会失落在页面上输出的数据。
当新导航达到与以后出现的站点不同的站点时,将调用一个独自的出现过程来解决新的导航,同时保留以后的出现过程以解决诸如 unload
。无关页面生命周期状态,能够看 这里。
下图为从浏览器过程到新渲染器过程的 2 个 IPC,通知渲染页面并通知旧渲染器过程卸载:
Service Worker
首先,Service Worker 容许开发者更好地管制本地缓存的内容以及何时从网络获取新数据。如果 service worker 设置为从缓存加载页面,则无需从网络申请数据。
留神:Service Worker 是在渲染器过程中运行的 JavaScript 代码。
然而当导航申请进来时,浏览器过程又如何晓得哪个站点有Service Worker?
注册Service Worker后,Service Worker的作用域将会保留。当导航产生时,网络线程会依据注册的 Service Worker 范畴查看域,如果 Service Worker 已为该 URL 注册,则 UI 线程会查找渲染器过程以执行 Service Worker 代码。Service Worker 可能会从缓存中加载数据,从而无需从网络申请数据,或者它可能会从网络申请新资源。
下图为浏览器过程中的 UI 线程启动渲染器过程来解决服务工作者;渲染器过程中的工作线程而后从网络申请数据:
导航预加载
如果 Service Worker 最终决定从网络申请数据,浏览器过程和渲染器过程之间的这种往返可能会导致提早。Navigation Preloads
是一种通过在 Service Worker 启动的同时加载资源来减速此过程的机制。它用标头标记这些申请,容许服务器决定为这些申请发送不同的内容;例如,只是更新数据而不是残缺文档。
三、渲染
导航过后,浏览器会调用渲染器(UI)过程工作。
渲染器过程解决Web
渲染器过程负责选项卡内产生的所有事件。在渲染器过程中,主线程解决发送给用户的大部分代码。如果应用 Web Worker 或 Service Worker,有局部 JavaScript 由工作线程解决。合成器和光栅线程也在渲染器过程内运行,以高效、流畅地渲染页面。
渲染器过程的外围工作是将 HTML、CSS 和 JavaScript 转换为用户能够与之交互的网页。
解析
构建DOM
当渲染过程接管提交音讯用于导航和开始接管HTML数据,主线程开始解析HTML,使之成为一个 DOM
。
DOM 是浏览器对页面的外部示意,也是开发人员能够通过 JavaScript 与之交互的数据结构和 API。将HTML文档解析为DOM是由HTML规范定义的,所以有时候写错标签,也会被主动纠正,具体能够查看 解析器中的错误处理。
子资源加载
对于图像、CSS 和 JavaScript 等内部资源,须要从网络或缓存加载。主线程能够在解析构建DOM的过程中找到它们后一一申请,但为了加快速度,“预加载扫描器(preload scanner)” 是并发运行的。如果HTML 文档中有相似<img>
或 <link>
,预加载扫描器会查看 HTML 解析器生成的 token
,并将申请发送到浏览器过程中的网络线程。
JavaScript 能够阻止解析
当 HTML 解析器找到一个<script>
标签时,它会暂停 HTML 文档的解析,并且必须加载、解析和执行 JavaScript 代码。为什么?因为 JavaScript 能够应用document.write()
扭转整个 DOM 构造的货色来扭转文档的构造,这里有张图表。
如何加载资源
如果JavaScript 不应用document.write()
,能够增加 async
或 defer
属性到<script>
标签。而后浏览器异步加载和运行 JavaScript 代码,并且不会阻止解析。浏览器反对的话,当然也能够用 Javascript Module
。<link rel="preload">
是一种告诉浏览器以后导航必定须要该资源并且心愿尽快下载的形式,这里是资源优先级。
款式解析
主线程会解析 CSS 并确定每个 DOM 节点的计算款式。
每个DOM节点都有一个默认款式,这是默认样式表。
布局
到目前为止,渲染器过程晓得文档的构造和每个节点的款式。
布局是一个寻找元素几何形态的过程,主线程遍历 DOM 和计算款式并创立布局树,其中蕴含 xy 坐标和边界框大小等信息。布局树可能与 DOM 树的构造类似,但它只蕴含与页面上可见的内容相干的信息。如果 display: none
利用,则该元素不是布局树的一部分(然而,具备的 visibility: hidden
在布局树中)。相似地,如果利用了具备相似内容的伪类,p::before{content:"Hi!"}
即便它不在 DOM 中,它也会蕴含在布局树中。
CSS代表了整个页面的初始布局,如果想多理解一点,看下这个演讲吧!
绘制
到目前为止,有了DOM、款式和布局,然而想要开始绘制须要判断绘制的程序。例如,z-index
可能会为某些元素设置,在这种状况下,依照 HTML 中编写的元素的程序绘制将导致不正确的渲染。
在绘制步骤中,主线程遍历布局树以创立绘制记录。绘制记录的程序是:先背景,后文字,再矩形。这个和 <canvas>
的绘制过程有点像。
留神
绘制过程最重要的一点是:绘制的每一步都应用前一操作的后果来创立新数据。如果布局树中的某些内容产生了变动,则须要为文档的受影响局部从新生成绘制程序。
如果为元素设置动画,则浏览器必须在每一帧之间运行这些操作。咱们的大多数显示器每秒刷新屏幕 60 次 (60 fps);在每一帧在屏幕上挪动物体时,动画对人眼来说会显得平滑。然而,如果动画错过了两头的帧,则页面将呈现“janky”。
即便渲染操作跟上屏幕刷新,这些计算也在主线程上运行,也就是说当利用程序运行 JavaScript 时,它可能会被阻止。
这时候,能够将 JavaScript 操作分成小块,并应用 requestAnimationFrame()
来解决,也能够通过 WebWorker
运行JavaScript以防止阻塞主线程。无关JS执行优化,能够点这里。
合成
光栅化
到当初,浏览器晓得了文档的构造、每个元素的款式、页面的几何形态和绘制程序,开进行真正的绘制,将此过程转换为屏幕上的像素称为 光栅化
。
Chrome第一次公布时,解决光栅化的形式是:只在视窗口内对局部页面进行光栅化,当用户滚动页面,就挪动光栅的架子,并通过更多光栅来填充缺失的局部。
然而,在古代浏览器运行着一个更简单的过程,叫合成。
合成,把页面的各个局部分成多个层,独自光栅化它们,并在合成器线程的独自线程中合并成一个页面。此时如果产生滚动,因为图层曾经被光栅化,它所要做的就是合成一个新的框架。动画能够通过挪动图层并合成新帧以雷同的形式实现。查看页面的图层,能够从控制台的 More tools --> layers
关上。
分层
为了找出哪些元素须要在哪些层中,主线程遍历布局树以创立层树(能够在 DevTools 的 Performance 面板中称为“Update Layer Tree”)。如果页面的某些局部应该是独自的层(如滑入式侧菜单)没有获取到,能够通过应用 will-change
CSS 中的属性来提醒浏览器。
和每帧光栅化页面的小局部相比,为每个元素都提供层,并且合成会导致操作很慢。
主线程的光栅和合成
一旦创立了层树并确定了绘制程序,主线程就会将该信息提交给合成器线程。合成器线程而后光栅化每一层。一个图层可能像页面的整个长度一样大,因而合成器线程将它们分成多个图块并将每个图块发送到光栅线程。光栅线程光栅化每个图块并将它们存储在 GPU 内存中。
合成器线程能够对不同的光栅线程进行优先级排序,以便能够首先对视口内(或左近)的事物进行光栅化。一个图层也有多个不同分辨率的平铺来解决诸如放大操作之类的事件。
对切片进行光栅化后,合成器线程会收集称为绘制四边形的切片信息以创立合成器框架。
名称 | 阐明 |
---|---|
合成器框架 | 代表页面框架的绘制四边形的汇合。 |
绘制四边形 | 蕴含诸如磁贴在内存中的地位以及在思考页面合成的状况下在页面中绘制磁贴的地位等信息。 |
而后通过 IPC 将合成器框架提交给浏览器过程。此时,能够从用于浏览器 UI 更改的 UI 线程或用于扩大的其余渲染器过程增加另一个合成器框架。这些合成器帧被发送到 GPU 以将其显示在屏幕上。如果呈现滚动事件,合成器线程会创立另一个合成器帧以发送到 GPU。
合成的益处是它是在不波及主线程的状况下实现的。合成器线程不须要期待款式计算或 JavaScript 执行。这就是为什么 只合成动画
被认为是取得晦涩性能的最佳抉择。如果须要从新计算布局或绘制,则必须波及主线程。
用户输出和合成器
浏览器的input事件
从浏览器的角度来看,输出意味着来自用户的任何事件。鼠标滚轮滚动是一个事件,触摸或鼠标悬停也是一个事件。
当用户在屏幕上进行触摸等手势时,浏览器过程首先接管该手势。然而,浏览器过程只晓得该手势产生的地位,因为选项卡内的内容由渲染器过程解决。因而浏览器过程将事件类型(如touchstart
)及其坐标发送到渲染器过程。渲染器过程通过查找事件指标并运行附加的事件侦听器来适当地处理事件。
下图为Input事件通过浏览器过程路由到渲染器过程:
非疾速滚动区域
如果没有input事件监听器附加到页面,合成器线程能够创立一个齐全独立于主线程的新复合框架,如果某些事件侦听器附加到页面上,合成器线程如何确定事件是否须要解决?
因为运行 JavaScript 是主线程的工作,因而在合成页面时,合成器线程会将页面中附加有事件处理程序的区域标记为“非疾速可滚动区域”。通过取得这些信息,合成器线程能够确保在该区域产生事件时将input事件发送到主线程。如果input事件来自该区域之外,则合成器线程持续合成新帧,而无需期待主线程。
事件委托
开发中常见的事件处理模式是事件委托。因为事件冒泡,能够在最顶层元素附加一个事件处理程序,并依据事件指标委派工作,比方:
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault(); }});
如果须要为所有元素编写一个事件处理程序的话,这种事件委托模式很有吸引力。然而,如果从浏览器的角度来看这段代码,当初整个页面都被标记为非疾速可滚动区域。这意味着即便程序不关怀来自页面某些局部的输出,合成器线程也必须与主线程通信并在每次输出事件进入时期待它。因而,合成器的平滑滚动能力被战胜了。
为了缩小这种状况的产生,能够传递属性 passive: true
,这样会向浏览器暗示依然心愿在主线程中侦听事件,但合成器也能够持续合成新帧,比方:
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } }, {passive: true});
查看事件是否能够勾销
有个场景,只有程度滚动,没有垂直滚动。
passive: true
在指针事件中应用选项意味着页面滚动能够平滑,但垂直滚动可能在想要的时候开始preventDefault
以限度滚动方向,这时候能够应用event.cancelable
办法对此进行查看,比方:
document.body.addEventListener('pointermove', event => { if (event.cancelable) { event.preventDefault(); // block the native scroll /* * do what you want the application to do here */ }}, {passive: true});
或者,能够应用 CSS 规定 touch-action
来齐全打消事件处理程序,比方:
#area { touch-action: pan-x;}
寻找event.target
当合成器线程向主线程发送输出事件时,首先要运行的是命中以找到事件指标。命中应用渲染过程中生成的绘制记录数据来找出产生事件的点坐标下方的内容。
最小化事件调度到主线程
后面晓得了,典型显示器每秒刷新屏幕 60 次,以及咱们须要跟上节奏以取得晦涩的动画。而对于输出来说。典型的触摸屏设施每秒传递 60-120 次触摸事件,典型的鼠标每秒传递 100 次事件。输出事件的保真度高于咱们的屏幕能够刷新的保真度。
如果像touchmove
这样的间断事件每秒发送到主线程 120 次,那么与屏幕刷新的速度相比,它可能会触发过多的命中和 JavaScript 执行:
为了尽量减少对主线程的过多调用,Chrome 会合并间断事件(例如 wheel
, mousewheel
, mousemove
, pointermove
, touchmove
)并提早调度直到下一个requestAnimationFrame
,能够发现,工夫线一样,但事件进行了合并和提早。
相似的事件,如keydown
,keyup
,mouseup
,mousedown
,touchstart
,和touchend
被立刻执行。
应用 getCoalescedEvents
失去帧内事件
对于大多数 Web 程序,合并事件应该足以提供良好的用户体验。然而,像构建绘图程序和基于 touchmove
坐标搁置门路之类的货色 ,绘制平滑线的时候可能会失落两头坐标。在这种状况下,能够应用 getCoalescedEvents
指针事件中的办法来获取无关这些合并事件的信息。
下图左侧是平滑的触摸手势门路,右侧是合并的无限门路:
window.addEventListener('pointermove', event => { const events = event.getCoalescedEvents(); for (let event of events) { const x = event.pageX; const y = event.pageY; // draw a line using x and y coordinates. }});
参考资料
Inside look at modern web browser