这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战javascript
这篇文章咱们将从高级架构到渲染管道的细节深刻了解 Chrome 浏览器。若是您想知道浏览器如何将您的代码转换为功能性网站,或者您不肯定为何建议使用特定技术来提升性能,那么本文适合您。css
对于前端开发者,知其然知其因此然很重要,因此只有在知己知彼的状况下才能更好的驾驭浏览器,以期浏览器给你带来更高的性能。html
CPU(中央处理器) 和 GPU(图形处理器) 做为计算机中最重要的两个计算单元直接决定了计算性能。前端
CPU 能够被认为是您计算机的大脑。与 CPU 不一样,GPU 擅长处理简单的任务,但同时跨多个内核。顾名思义,它最初是为处理图形而开发的。这就是为何在图形上下文中“使用 GPU”或“GPU 支持”与快速渲染和流畅交互相关联。近年来,随着 GPU 加速计算,愈来愈多的计算成为可能单独使用 GPU。java
当您在计算机或手机上启动应用程序时,CPU 和 GPU 是驱动应用程序的驱动力。一般,应用程序使用操做系统提供的机制在 CPU 和 GPU 上运行。web
咱们能够把计算机自下而上分红三层:底部是机器硬件,中间是操做系统,顶部是应用程序。有了操做系统的存在,上层运行的应用可使用操做系统提供的能力使用硬件资源而不会直接访问硬件资源。chrome
进程做为边界框,线程做为在进程内游动的抽象鱼api
在深刻研究浏览器架构以前要掌握的另外一个概念是进程和线程
。一个进程
能够被描述为一个应用程序的执行程序。线程
是存在于进程内部并执行其进程程序的任何部分的线程。跨域
当您启动应用程序时,就会建立一个进程。程序可能会建立线程来帮助它工做,但这是可选的。操做系统会为进程分配私有的内存空间以供使用,当关闭程序时,这段私有的内存也会被释放。其实还有比线程更小的存在就是协程,而协成是运行在线程中更小的单位。async/await 就是基于协程实现的。浏览器
进程间通讯(IPC):一个进程可让操做系统开启另外一个进程处运行不一样的任务。当两个进程须要通讯时,能够用 IPC(Inter Process Communication)。
多数程序被设计成使用 IPC 来进行进程间的通讯,好处在于当一个进程给另外一个进程发消息而没有回应时,并不影响当前的进程继续工做。
那么如何使用进程和线程构建 Web 浏览器呢?嗯,它多是一个进程有许多不一样的线程,也多是许多不一样的进程有几个线程经过 IPC 进行通讯。
这里须要注意的重要一点是,这些不一样的架构是实现细节。没有关于如何构建 Web 浏览器的标准规范。一种浏览器的方法可能与另外一种彻底不一样。
在这里咱们将使用下图中描述的 Chrome 最新架构。
顶部是浏览器进程与其余处理应用程序不一样部分的进程协调。对于渲染器进程,会建立多个进程并将其分配给每一个选项卡。直到最近,Chrome 还在可能的状况下为每一个选项卡提供了一个进程;如今它尝试为每一个站点提供本身的进程,包括 iframe(请参阅站点隔离)。
下表描述了每一个 Chrome 进程及其控制的内容:
流程及其控制的内容 | |
---|---|
浏览器 | 控制应用程序的“chrome”部分,包括地址栏、书签、后退和前进按钮。 还处理 Web 浏览器的不可见的特权部分,例如网络请求和文件访问。 |
渲染器 | 控制显示网站的选项卡内的任何内容。 |
插件 | 控制网站使用的任何插件,例如 flash。 |
图形处理器 | 独立于其余进程处理 GPU 任务。它被分红不一样的进程,由于 GPU 处理来自多个应用程序的请求并将它们绘制在同一个表面上。 |
还有更多的进程,如扩展进程和实用程序进程。若是您想查看 Chrome 中正在运行的进程数,请单击选项菜单图标 more_vert在右上角,选择更多工具,而后选择任务管理器。这将打开一个窗口,其中包含当前正在运行的进程列表以及它们使用的 CPU/内存量。
Chrome 使用多个渲染器进程。在最简单的状况下,您能够想象每一个选项卡都有本身的渲染器进程。假设您打开了 3 个选项卡,每一个选项卡都由一个独立的渲染器进程运行。若是一个选项卡变得无响应,那么您能够关闭无响应的选项卡并继续前进,同时保持其余选项卡的活动。若是全部选项卡都在一个进程上运行,当一个选项卡变得无响应时,全部选项卡都无响应。那只能重启浏览器了。
将浏览器的工做分红多个进程的另外一个好处是安全性和沙盒。
由于进程有本身的私有内存空间,因此它们一般包含公共基础设施的副本(好比 V8,它是 Chrome 的 JavaScript 引擎)。这意味着更多的内存使用量,由于若是它们是同一进程内的线程,它们就不能像它们那样共享。为了节省内存,Chrome 限制了它能够启动的进程数。该限制取决于您设备的内存和 CPU 能力,但当 Chrome 达到限制时,它会开始在一个进程中运行来自同一站点的多个选项卡。
让咱们看一个简单的 Web 浏览用例:您在浏览器中键入一个 URL,而后浏览器从 Internet 获取数据并显示一个页面。在这里咱们将重点介绍用户请求网站和浏览器准备呈现页面的部分 - 也称为导航。
当您在地址栏中键入 URL 时,您的输入由浏览器进程的 UI 线程处理。
1. 处理输入
当用户开始在地址栏中键入内容时,UI 线程首先询问的是“这是搜索查询仍是 URL?”。在 Chrome 中,地址栏也是一个搜索输入字段,所以 UI 线程须要解析并决定是将您发送到搜索引擎,仍是发送到您请求的站点。
由于 Chrome 浏览器的地址栏既能够当作地址栏也能够当作搜索栏
2. 开始访问
当用户按下回车键时,UI 线程会发起网络调用以获取站点内容。浏览器页签的标题上会出现加载中的图标,网络线程经过适当的协议,如 DNS 查找域名并请求服务器创建 TLS 链接。
当服务器返回给浏览器重定向请求时,网络线程会通知 UI 线程须要重定向,而后会以新的地址作开始请求资源。
当服务器返回给浏览器重定向请求时,网络线程会通知 UI 线程须要重定向,而后会以新的地址作开始请求资源。
3. 处理响应数据
当网络线程收到来自服务器的数据时,会试图从数据中的前面的一些字节中获得数据的类型(Content-Type
),以试图了解数据的格式。
当返回的数据类型是 HTML 时,会将数据传递给渲染进程作进一步的渲染工做。可是若是数据类型是 zip 文件或者其余文件格式时,会将数据传递给下载管理器作进一步的文件预览或者下载工做。
在开始渲染以前,网络线程要先检查数据的安全性,这里也是浏览器保证安全的地方。若是返回的数据来自一些恶意的站点,网络线程会显示警告的页面。同时,Cross Origin Read Blocking(CORB)策略也会确保跨域的敏感数据不会被传递给渲染进程。
4. 渲染过程
当全部的检查结束后,网络线程确信浏览器能够访问站点时,网络线程通知 UI 线程数据已经准备好了。UI 线程会根据当前的站点找到一个渲染进程完成接下来的渲染工做。
在第二步,UI 线程将请求地址传递给网络线程时,UI 线程就已经知道了要访问的站点。此时 UI 线程就能够开始查找或启动一个渲染进程,这个动做与让网络线程下载数据是同时的。若是网络线程按照预期获取到数据,则渲染进程就已经能够开始渲染了,这个动做减小了从网络线程开始请求数据到渲染进程能够开始渲染页面的时间。固然,若是出现重定向的请求时,提早初始化的渲染进程可能就不会被使用了,但相比正常访问站点的场景,重定向每每是少数,在实际工做中,也须要根据特定的场景给出特定的方案,没必要追求完美的方案。
5. 提交访问
经历前面的步骤,数据和渲染进程都已经准备好了。浏览器进程会经过 IPC 向渲染进程提交此次访问,同时也会保证渲染进程能够经过网络线程继续获取数据。一旦浏览器进程收到来自渲染进程的确认完毕的消息,就意味着访问的过程结束了,文档渲染的过程就开始了。
这时,地址栏显示出代表安全的图标,同时显示出站点的信息。访问历史中也会加入当前的站点信息。为了能恢复访问历史信息,当页签或窗口被关闭时,访问历史的信息会被存储在硬盘中。
额外步骤. 加载完毕
当访问被提交给渲染进程,渲染进程会继续加载页面资源而且渲染页面。当渲染进程"结束"渲染工做,会给浏览器进程发送消息,这个消息会在页面中全部子页面(frame)结束加载后发出,也就是 onLoad 事件触发后发送。当收到"结束"消息后,UI 线程会隐藏页签标题上的加载状态图标,代表页面加载完毕。
但这里"结束"并不意味着全部的加载工做都结束了,由于可能还有 JavaScript 在加载额外的资源或者渲染新的视图。
一次普通的访问到此就结束了。当咱们输入另一个地址时,浏览器进程会重复上面的过程。可是在开始新的访问前,会确认当前的站点是否关心beforeunload
事件。
beforeunload
事件能够提醒用户是否要访问新的站点或者关闭页签,若是用户拒绝则新的访问或关闭会被阻止。
因为全部的包括渲染、运行 Javascript 的工做都发生在渲染进程中,浏览器进程须要在新的访问开始前与渲染进程确认当前的站点是否关心unload
。
若是一次访问是从一个渲染进程中发起的,例如用户点击一个连接或者运行 JavaScript 代码location = 'http://newsite.com'
时,渲染进程首先检查beforeunload
。而后再执行和浏览器进程初始化访问一样的步骤,只不过区别在于这样的访问请求是由渲染进程向浏览器进程发起的。
当新的站点请求被建立时,一个独立的渲染进程将被用于处理这个请求。为了支持像unload
的事件触发,老的渲染进程须要保持住当前的状态。更详细的生命周期介绍能够参考Page lifecycle。
Service worker 是一种能够 web 开发者控制缓存的技术。若是 Service worker 被实现成从本地存储获取数据时,那么本来的请求就不会被浏览器发送给服务器了。
值得注意的是,Service worker 中的代码是运行在渲染进程中的。当访问开始时,网络线程会根据域名检查是否有 Service worker 会处理当前地址的请求,若是有,则 UI 线程会找到对应的渲染进程去执行 Service worker 的代码,而 Service worker 可让开发者决定这个请求是从本地存储仍是从网络中获取数据。
若是 Service worker 最终决定要从网络中获取数据时,咱们会发现这种跨进程的通讯会形成一些延迟。Navigation Preload是一种能够在 Service worker 启动的同时加载资源的优化机制。借助特殊的请求头,服务器能够决定返回什么样的内容给浏览器。
渲染进程负责全部发生在浏览器页签中的事情。在一个渲染进程中,主线程负责解析,编译或运行代码等工做,当咱们使用 Worker 时,Worker 线程会负责运行一部分代码。合成线程和光栅线程是也是运行在渲染进程中的,负责更高效和顺畅的渲染页面。
渲染进程最重要的工做就是将 HTML、CSS 和 Javascript 代码转换成一个能够与用户产生交互的页面。
renderer.png
下面的章节主要介绍渲染进程如何将从网络线程中获取的文本转化成图像的过程。
当渲染进程接收到来自浏览器进程提交访问的消息后就开始接受 HTML 数据,主线程开始解析 HTML 文本字符串,而且将其转化成 Document Object Model(DOM) 。
DOM 是一种浏览器内部用于表达页面结构的数据,同时也为 Web 开发者提供了操做页面元素的接口,让 web 开发者能够在 Javascript 代码中获取和操做页面中的元素。
将 HTML 文本转化成 DOM 的标准被HTML Standard定义。咱们会发如今转化过程当中浏览器历来不会抛出异常,相似关闭标签的丢失,开始、关闭标签匹配错误等等。这是由于 HTML 标准中定义了要静默的处理这些错误,若是对此感兴趣能够阅读An introduction to error handling and strange cases in the parser。
一个网站一般还会使用相似图片,样式文件和 JavaScript 代码等额外的资源。这些资源也须要从网络或缓存中获取。主线程在转化 HTML 的过程当中理应挨个加载它们,可是为了提升效率,预加载扫描(Preload Scanner)与转换过程会同时运行着。当预加载扫描在分析器分析 HTML 过程当中发现了相似 img 或 link 这样的标签时,就会发送请求给浏览器进程的网络线程,而主线程会根据这些额外资源是否会阻塞转化过程而决定是否等待资源加载完毕。
当 HTML 分析器发现<script>
标签时,会暂停接下来的 HTML 转化工做,而后加载、解析而且运行 Javascript 代码。由于在 Javascript 代码中可能会使用相似document.write
这样的 API 去改变 DOM 的结构。这就是为何 HTML 分析器必须等待 Javascript 代码运行结束才能继续分析的缘由。
若是咱们的 Javascript 代码并不须要改变 DOM,能够为<script>
标签添加async
或defer
属性,这样浏览器就会异步的加载这些资源而且不会阻塞 HTML 转化过程。若是 script 标签是由 JavaScript 代码建立的,标签的 async 属性会默认为 true。 同时咱们也可使用一些预加载技术,好比<link ref="preload">
来通知浏览器这些资源须要越快下载越好。
对于展现一个页面,光有 DOM 是不够的,由于咱们还须要样式来让页面变得更美观。主线程会解析样式(CSS)并决定每一个 DOM 元素的样式。这些样式取决于 CSS 选择器的范围,在浏览器开发者工具中咱们能够看到这些信息。
computedstyle.png
即便咱们没有给 DOM 指定任何的样式,<h1>
标签也会比<h2>
标签显示的大。这是由于浏览器为不一样的标签内置了不一样的样式。能够经过Chromium源代码获得这些默认样式。
完成了样式计算工做后,渲染进程已经知道了 DOM 的结构和每一个节点的样式,可是依然不足以渲染一个页面。
布局是为元素指定几何信息的过程。主线程遍历 DOM 结构中的元素及其样式,同时建立出带有坐标和元素尺寸信息的布局树(Layout tree)。布局树的结构与 DOM 树的结构十分类似,但只包含将会在页面中显示的元素。当一个元素的样式被设置成 display: none 时,元素就不会出如今布局树中,但那些样式被设置成 visiblility:hidden 的元素会出如今布局树中。 类似的,当咱们使用一个包含内容的伪元素(例如p::before { content: 'Hi!' }
)时,元素会出如今布局树中即便这个元素不存在于 DOM 树中,这也是为何咱们使用 DOM 提供的 API 没法获取伪元素的缘由。
描述页面布局信息是一项具备挑战性的工做,即便在只有块元素的页面中也必需要考虑字体的大小和在哪里换行,由于在计算下一个元素的位置时须要知道上一个元素的尺寸和形状。
CSS 可让元素浮动、可让元素在父元素中溢出,能够改变文字的方向。能够想象,在布局这个阶段是多么繁重的工做。在 Chrome 中,有一整个团队在维护布局工做,更详细的信息能够观看视频。
有了 DOM、样式和布局仍是没法完成渲染工做。试想,当咱们试图复制一张图画。咱们知道图画中元素的尺寸、形状和位置,咱们还须要知道绘制这些元素的顺序。
在这个阶段,主线程遍历布局树并建立绘制记录,绘制记录是一系列由绘制步骤组成的流程,例如先绘制背景,而后是文字,而后是形状。
在渲染过程当中,任何一个步骤中产生的数据变化都会引发后续一系列的的变化。例如,当布局树改变时,绘制须要重构页面中变化的部分。
当一些元素有动画发生时,浏览器须要在每一帧中绘制这些元素。当没法保证每一帧绘制的连续性时,用户就会感受到卡顿。
正常状况下渲染操做能够与屏幕刷新保持同步,但因为这些操做运行在主线程中,也就意味这些操做可能被正在运行的 Javascript 代码所阻塞。
为了避免影响渲染操做,咱们能够将 Javascript 操做优化成小块,而后使用requestAnimationFrame()
,关于如何优化能够参考Optimize JavaScript Exectuion。当须要大量计算时,也可使用 Worker 来避免阻塞主进程。
如今,浏览器已经知道了文档结构、每个元素的样式,元素的几何信息,绘制的顺序。将这些信息转化成屏幕上像素的过程叫作光栅化,光栅化是图形学的范畴。
传统的作法是将可视区域的内容进行光栅化。随着用户滚动页面,不断的光栅化更多的区域。然而对于现代浏览器,有着更复杂的的过程,这个过程被称作合成。
合成是一种将页面拆分红多层的技术,合成线程能够将各个层在不一样线程中光栅化,再组合成一个页面。当滚动时,若是层已经被光栅化,则会使用已经存在的层合成新的帧,动画则能够经过移动层来实现。
为了决定层包含哪些元素,主线程须要遍历布局树以找到须要生成的部分。对开发者来讲,当某一部分须要用独立的层渲染,咱们可使用 css 属性will-change
让浏览器建立层,关于浏览器如何生成层的标准可自行查阅。
虽然经过分层能够优化浏览器性能,但并不意味着应该给每一个元素一个层,过多的层反而影响性能,因此在层的划分上应该具体形况具体分析。
当布局树和绘制顺序肯定之后,主线程会将这些信息提交给合成线程。合成线程会光栅化各个层。一个层包含的内容多是一个完整的页面,也多是页面的部分,因此合成线程将层拆分红许多块,并将它们发送给栅格线程。栅格线程光栅化这些块并将它们存储在 GPU 缓存中。
合成线程能够决定栅格线程光栅块的优先级,这样能够保证用户能看到的部分能够先被光栅化。一个层也会包含多种块以支持相似缩放这样的功能。
当块被光栅化后,合成线程会使用 draw quads 收集这些信息并建立合成帧(Compositor frame)。
存储在缓存中,包含相似块位置这样的信息,用于描述如何使用块合成页面。
用于存储表现页面一帧中包含哪些 Draw quads 的集合。
而后一个合成帧被提交给浏览器进程。这时若是浏览器 UI 有变化,或者插件的 UI 有变化时,另外一个合成帧就会被建立出来。因此每当有交互发生时,合成线程就会建立更多的合成帧而后经过 GPU 将新的部分渲染出来。
合成的好处在于其独立于主线程。合成线程不须要等待样式计算和 Javascript 代码的运行。这也是为何合成更适合优化交互性能,但若是布局或者绘制须要从新计算则主线程是必需要参与的。
本质上,浏览器的渲染过程就是将文本转换成图像的过程,而当用户与页面发生交互动做时,则显示新的图像。在这个过程当中由渲染进程中的主线程完成计算工做,由合成线程和栅格线程完成图像的绘制工做。而在计算过程当中,还有强制布局、重排、重绘等更加细节的概念会在后面的文章中作讲解。
当咱们听到事件时,一般会联想到在一个文本框中输入或者单击鼠标,但从浏览器的角度看,输入事件意味着全部的用户动做。鼠标滚轮滚动或者屏幕触摸都是输入事件。
当用户与页面发生交互时,浏览器进程首先接收到事件,然而,浏览器进程只关心事件发生时是在哪一个页签中,因此浏览器进程会将事件类型和位置信息等发送给负责当前页签的渲染进程,渲染进程会恰当的找到事件发生的元素而且触发事件监听器。
在前面的章节中,咱们知道了合成线程能够经过合成技术合成不一样的光栅层优化性能,若是页面并不监放任何事件,合成线程能够彻底独立于主线程生成新的合成帧。但若是页面监听了事件呢?
因为运行 Javascript 是主线程的工做,当页面被合成线程合成过,合成线程会标记那些有事件监听的区域。有了这些信息,当事件发生在响应的区域时,合成线程就会将事件发送给主线程处理。若是在非事件监听区域,则渲染进程直接建立新的帧而不关心主线程。
在 web 开发中常见的方式就是事件代理。利用事件冒泡,咱们能够在目标元素的上层元素中监听事件。参照下面的代码。
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault(); }});
复制代码
经过这种写法,能够更高效的监听事件。但若是从浏览器的角度看,此时整个页面会被标记成“慢滚动”区域。这意味着虽然页面中的某些部分并不须要事件监听,但合成线程依然要在每次交互发生后等待主线程处理事件,合成线程的优化效果不复存在。
为了解决这个问题,咱们可在事件代理时传入passive: true
(IE 不支持) 参数。这样告诉渲染线程,依然须要将事件发送给主线程处理,但不须要等待。
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } }, {passive: true});
复制代码
关于使用 passive 改善滚屏性能,能够参考MDN 使用passive改善滚屏性能。
当渲染线程将事件发送给主线程后,第一件事就是找到事件触发的目标。经过在渲染过程当中生成的绘制信息,能够根据坐标找到目标元素。
为了保证动画的顺畅,须要显示器在每秒刷新 60 次。对于典型的触摸事件由合成线程提交给主线程的事件频率能够达到每秒 60-120 次,对于典型的鼠标事件每秒会发送 100 次。事件发送的频率一般比屏幕刷新频率要高。
若是相似touchmove
这样的事件每秒向主线程发送 120 次可能会形成主线程执行时间过长而影响性能。
为了减小发送给主线程的事件数量,Chrome 合并了连续的事件。相似wheel
,mousewheel
,mousemove
,pointermove
,touchmove
这样的事件会被延迟到下一次requestAnimationFrame
前触发.
而任何的离散事件,相似keydown
, keyup
, mouseup
, mousedown
, touchstart
和 touchend
都会当即被发送给主线程处理。
到此,咱们已经能够经过从用户在浏览器地址栏中的一次输入到页面图像的显示了解浏览器是如何工做的。这里咱们总结一下。
最后,感谢你的阅读。
文中若有错误,欢迎在评论区指正,若是这篇文章帮到了你,欢迎点赞👍和关注😊,但愿点赞多多多多...