前端基础:浏览器初识

1,630 阅读25分钟

深入了解现代浏览器

第一部分

CPU,GPU,内存和多进程架构

计算机的核心是CPUGPU,为了了解浏览器正在运行的环境,我们还需要了解一些计算机部件及其功能。

中央处理器(CPU)

CPU可以被视为计算机的大脑。一个CPU内核,可以处理许多不同的任务。它可以处理从美术到数学的所有工作,同时又知道如何回复客户的电话。过去,大多数CPU都是单芯片。内核就像生活在同一个芯片中的另一个CPU。在现代硬件中,通常具有多个内核,从而为手机和笔记本电脑提供更多的计算能力。

显卡(GPU)

CPU不同,GPU擅长处理简单任务,但同时跨多个内核。它最初是为处理图形而开发。这就是为什么在图形上下文中使用GPU支持GPU与快速渲染和平滑交互相关联的原因。

  • 三层计算机体系结构。机器硬件在底部,操作系统在中间,应用程序在顶部。

在进程和线程上执行程序

在深入浏览器体系结构之前要掌握的另一个概念是Process And Thread.进程可以描述为应用程序的执行程序。线程存在于流程内部并执行其流程程序的任何部分的线程。

启动应用程序时,将创建一个线程。该程序可能会创建线程来帮助其工作,但这是可选的。操作系统为进程提供了一个平板的内存,所有应用程序状态都保留在该专用内存空间中。当您关闭程序时,该过程也会消失,并且操作系统会释放内存。

一个进程可以要求操作系统启动另一个进程来运行不同的任务。发生这种情况时,将为新进程分配内存的不同部分。如果两个进程需要通信,他们可以利用IPC(进程间通信)。许多应用程序都以这种方式工作,如果工作进程无响应,则可以重新启动它,而无需停止正在运行应用程序不同部分的其他进程。

  • 通过IPC进行通信

浏览器架构

那么如何使用进程和线程构建web浏览器?它可以是一个具有许多不同线程的进程,也可以是有几个通过IPC进行通信的多个线程的进程。

  • 进程/线程图中的不同浏览器架构 关于如何构建网络浏览器,没有标准规范。一种浏览器的方法可能与另一种完全不同。

顶部是浏览器进程,它与负责应用程序不同部分的其他进程进行协调。对于渲染器进程,将创建多个进程并将其分配给每个选项卡。

  • Chrome的多进程架构图。在“渲染器进程”下显示了多个图层,以表示Chrome为每个选项卡运行了多个“渲染器进程”。 | 过程 | 控制 | | ------------- |:-------------:| | 浏览器 | 控制应用程序的“chrome”部分,包括地址栏,书签,后退和前进按钮。 | | 渲染器 | 控制显示网站的选项卡的所有内容 | | 插件 | 控制网站使用的所有插件,例如flash | | 显卡 | 与其他进程隔离地处理GPU任务。由于GPU处理来自多个应用程序的请求并将它们绘制在同一表面上,因此将其分为不同的过程 |
  • 指向浏览器UI不同部分的不同过程

chrome中多进程架构的好处

之前,我提到Chrome使用多个渲染器过程。在最简单的情况下,您可以想象每个选项卡都有其自己的渲染器过程。假设您有3个标签页处于打开状态,每个标签页均由独立的渲染器进程运行。如果一个选项卡变得无响应,则可以关闭无响应的选项卡并继续运行,同时保持其他选项卡的活动状态。如果所有选项卡都在一个进程上运行,则当一个选项卡无响应时,所有选项卡将无响应

  • 运行每个选项卡的多个进程 由于进程具有自己的私有内存空间,因此它们通常包含通用基础结构的副本(例如V8,这是ChromeJavaScript引擎)。这意味着更多的内存使用情况,因为如果它们是同一进程中的线程,将无法共享它们。为了节省内存,Chrome对可启动的进程数量进行了限制。该限制取决于设备拥有的内存CPU能力,但是Chrome达到限制后,它将开始在同一过程中从同一站点运行多个标签页。

节省更多内存-Chrome中的服务化

Chrome正在进行架构整改,以此将浏览器程序的每个部分作为一项服务运行,从而可以轻松拆分为不同的进程或聚合为一个进程。

一般的想法是,当Chrome在功能强大的硬件上运行时,它可能会将每个服务拆分为不同的进程,从而提供更改的稳定性,但是在资源受限的设备上,Chrome会将服务整合到一个进程中,从而节省了内存。在此更改之前,已在类似Android平台上使用了类似的整合过程以减少内存使用量的方法。

  • Chrome服务化示意图,将不同的服务移至多个进程和一个浏览器进程

每帧渲染过程-站点隔离

网站隔离时Chrome最近引入的功能,可为每个跨网站iframe运行单独的渲染器进程。我们一直在讨论每个标签模型一个渲染器进程。该进程允许跨站点Iframe在单个渲染器进程中运行,并在不同站点之间共享内存空间。在相同的渲染器进程中运行a.com和b.com似乎可以。同源策略时网络的核心安全模型;这样就可以确保一个站点未经同意就无法访问其他站点的数据。绕过此策略时安全攻击的主要目标。进程隔离是分离站点最有效的方法。从Chrome67开始,默认情况下在桌面上启用“网站隔离”,因此标签中的每个跨网站iframe都会获得一个单独的渲染器进程

  • 站点隔离图;指向网站内iframe的多个渲染器进程 启用站点隔离是一项多年的工程工作。站点隔离并不像分配不同的渲染器进程那么简单。他从根本上改变了iframe彼此交流的方式。在页面上打开具有在不同进程上运行的iframe的devtools一意味着devtools必须实施幕后工作才能使其无缝显示。即使运行简单的Ctrl + F在页面中查找单词,也意味着跨不同的渲染器进程进行搜索。

第二部分

导航中会发生什么

在浏览器中输入URL,然后浏览器从internet上获取数据并显示一个页面。在本文中,我们将重点介绍用户请求站点和浏览器准备呈现页面(也称为导航)的部分

从浏览器过程开始

正如第一部分介绍那样,CPU,GPU,内存多进程体系结构中,选项卡之外的所有内容都以浏览器进程进行处理。浏览器进程具有以下线程:UI线程(用于绘制浏览器和按钮输入字段),网络线程(用于处理网络堆栈以从Internet接收数据),存储线程(用于控制对文件的访问)等等。在地址栏中键入URL时,输入将由浏览器进程的UI线程处理。

  • 顶部的浏览器UI,底部的UI,网路和存储线程的浏览器过程图

简单的导航

步骤1: 处理输入

当用户开始在地址栏中输入内容时,UI线程首先会判断“这是搜索查询还是URL?”.在Chrome浏览器中,地址栏也是搜索输入字段,因此UI线程需要解析并决定是将您发送到搜索引擎还是请求的网站。

步骤2:开始浏览

当用户按下enter键时,UI线程会发起网络调用以获取网站内容。加载微调器显示在选项卡的角上,并且网络线程通过相应的协议(例如DNS查找和为请求建立TLS连接)

  • UI线程与网络线程交谈以导航至mysite.com 此时网络线程可能会收到服务器重定向标头,例如HTTP301,网络线程与服务器正在请求重定向的UI线程进行通信。然后,启动另一个URL请求。

步骤3:读取响应

一旦响应主题开始进入,网络线程将在必要时查看流的前几个字节。响应的Content-Type标头应说明他是什么数据类型,但是由于可能丢失或错误,因此在此处进行MIME Type嗅探。

  • 响应头包含Content-Type和有效载荷,有效载荷是实际数据 如果时HTML文件,则下一步是将数据传递到渲染器进程,但是如果是ZIP或者其他文件,则意味着这是下载请求,因此他们需要将数据传递到下载管理器。
  • 网络线程从安全站点询问响应数据是否为HTML 这也在进行安全检查情况。如果域和响应数据似乎与已知的恶意站点匹配,则网络线程将会发出警报以显示警告页面。

步骤4:查找渲染器进程

一旦完成所有检查,并且NetWork线程确信浏览器应导航到请求的站点,则Network线程将告知UI线程数据已准备就绪。然后,UI线程找到一个渲染器进程来进行网页渲染。

  • 网络线程告诉UI线程查找渲染器进程 由于网络请求可能需要数百毫秒才能获得响应,因此将应用优化来加快此过程。当UI线程在步骤2向网络线程发送URL请求时,他已经知道他们正在导航到哪个站点。UI线程尝试与网络请求并行的主动查找或启动渲染器进程。这样,如果一切按照预期进行,则当网络线程接收数据时,渲染器进程已经处于备用位置。如果导航重定向跨站点,则可能不会使用此备用过程,在这种情况下,可能需要其他过程。

步骤5:提交导航

现在已经准备好数据和渲染器进程,将IPC从浏览器进程发送到渲染器进程以提交导航。他还会传递数据流,因此渲染器进程可以继续接收HTML数据。一旦浏览器进程听到确认已在渲染器进程中提交的确认,导航就完成了,文档加载阶段开始。 此时地址栏已更新,安全指示符和站点设置UI反映了新页面的站点信息。该选项卡的会话历史记录将被更新,因此后退、前进按钮将逐步浏览刚刚导航的站点,为方便在关闭选项卡或窗口时恢复选项卡/会话,会话历史记录存储在磁盘上。

  • 浏览器和渲染器进程之间的IPC,请求渲染页面

额外步骤:初始加载完成

提交导航后,渲染器将继续加载资源并渲染页面。在下一篇文章中将详细介绍此阶段发生的情况。渲染器进程“完成”选然后,它将IPC发送回浏览器进程。(这是在所有onload事件在页面中的所有框架上触发并执行完成之后)。此时UI线程在选项卡上停止加载微调器。 我说“完成”时因为客户端JavaScript仍然可以在此之后加载其他资源并呈现新视图。

  • 从渲染器到浏览器的IPC进程通知页面已“加载”

导航到其他站点

简单的导航就完成了!但是,如果用户再次将不同的URL放入地址栏会发生什么?好吧,浏览器过程将通过相同的步骤导航到不同的站点。但在此之前,它需要检查当前渲染的站点是否关心 beforeunload事件 beforeunload可以创建“离开此网站?” 尝试导航或关闭选项卡时发出警报。选项卡内的所有内容(包括您的JavaScript代码)都由渲染器进程处理,因此,当新的导航请求出现时,浏览器进程必须与当前渲染器进程进行核对。

  • 从浏览器过程到渲染器过程的IPC告诉它即将导航到另一个站点

如果导航是从渲染器进程启动的(例如用户单击链接或运行客户端JavaScript window.location = "newsite.com"),则渲染器进程首先检查beforeunload处理程序。然后,它经历与浏览器过程启动的导航相同的过程。唯一的区别是导航请求从渲染器进程到浏览器进程开始了。

当将新导航导航到与当前渲染站点不同的站点时,将调用一个单独的渲染进程来处理新导航,同时保留当前渲染进程来处理诸如之类的事件unload。

  • 从浏览器进程到新渲染器进程的2个IPC,它们告诉渲染页面并告诉旧渲染器进程卸载

Service Worker

Service Worker是在应用程序代码中编写网络代理的一种方法。使web开发人员可以更好的控制在本地缓存什么以及何时从网络获取新数据。如果Service Worker设置为从缓存加载页面,则无需从网络请求数据。 最重要的部分时Service Worker在渲染器进程中运行的JS代码。但是,当导航请求进入时,浏览器如何知道该站点的Service Worker。

导航预加载

如果Service Worker决定从网络请求数据,则浏览器与渲染器进程之间的这种往返可能会导致延迟。导航预加载是一种通过与Service Worker启动并行加载资源来加快此过程的机制。他用标头标记这些请求,使得服务器决定为这些请求发送不同的内容。例如,仅更新数据而不是完整文档。

  • 浏览器进程中的UI线程启动渲染器进程以处理Service Worker,同时启动网络请求

第三部分

渲染器进程处理Web内容

渲染器进程负责选项卡内部发生的所有事情。在渲染器进程中,主线程处理服务器发送给用户的大多数代码。如果使用webworker或者service worker,则又是javaScript的某些部分有worker线程处理。合成七线程和光栅线程也运行在渲染器进程内部,以高效,流畅的渲染页面。 渲染器进程的核心是将HTML,CSS,javaScript转换为用户可以与之交互的网页。

  • 具有主线程,辅助线程,合成器线程和内部光栅线程的渲染器进程

DOM的构造

当渲染过程接收提交消息用于导航和开始接收HTML数据,主线程开始解析文本串(HTML),使之成为一个文档对象。(DOM)

DOM是浏览器对页面的内部表示以及web开发人员可以通过Javascript与之交互的数据结构和API。 HTML标准定义了将HTML文档解析为DOM的功能。 将HTML文档发送到浏览器用于不会发送错误。

子资源加载

网站通常使用外部资源,例如图像,CSS和JAVASCRIPT,这些文件需要从网络和缓存中加载。主线程在解析HTML并构建DOM的过程中,找到他们时一个接一个的请求它们。但是为了加快速度,同时运行预加载程序,如果有像imglink在通过HTML解析器生成的标记在HTML文档中,预加载会发送请求到浏览器的网络线程中。

  • 主线程解析HTML并构建DOM树

JavaScript可以阻止解析 HTML解析器找到script标签后,他将暂停html文档的解析,并且必须加载,解析和执行javaScriprt代码。 因为JS可以使用document.write更改整个DOM结构之类的东西来更改文档的结构。

提示浏览器如何加载资源

Web开发人员可以通过多种方式将提示发送到浏览器,以便更好地加载资源。可以将async或添加defer到Script到script标签,然后,浏览器将异步加载并运行js代码,并且不会阻止解析。如果合适,也可以使用JS模块。 <link rel="preload">是一种通知浏览器该资源对于当前导航希望尽快下载。

style Computed

拥有DOM不足以知道页面的外观,因为我们可以在CSS中设置页面元素的样式。主线程解析CSS并确定每个DOM节点的计算样式。这是有关基于CSS选择器将哪种样式应用于每个元素的信息。

  • 主线程解析CSS以添加计算样式 即使不提供任何CSS,每个DOM节点页具有计算出的样式。h1标签显示大于h2标签,并且为每个元素定义了边距。这是因为浏览器具有默认样式表。

布局

现在,渲染器进程知道每个节点的文档结构和样式,但这不足以渲染页面。想象一下,您正在尝试通过电话向您的朋友描述一幅画。“有一个大的红色圆圈和一个小小的蓝色方块”不足以让您的朋友知道这幅画的模样。 布局是查找元素几何形状的过程。主线程遍历DOM和计算样式,并创建布局树,该树具有诸如xy坐标和边框大小之类的信息。布局树的结构可能与DOM树类型,但他仅包含与页面上可见内容有关的信息。如果dislay:none应用,则该元素不属于布局树(但是,具有visibility:hidden在布局树中)。同样,如果应用了具有类似伪类p::before,则即使他不在DOM中,它也将包含在布局书中。

  • 主线程遍历具有计算样式的DOM树并生成布局树

确定页面布局是一项艰巨的任务,即使是最简单的页面布局(如从上到下的块流程)也必须考虑字体的大小以及在何处换行,因为这会影响段落的大小和形状。这回影响下一段的位置。 CSS可以是元素浮动到一侧,掩盖溢出项,并改变书写方向。

Paint

拥有DOM,样式和布局任然不足以呈现页面。假设您正在尝试负责一幅画。您知道元素的大小,形状和位置,但任然需要判断以什么顺序绘制他们。

例如,z-index可能为某些元素设置了,但在这种情况下,按HTML中编写的元素顺序进行绘制将导致错误的呈现。

  • 页面元素以HTML标记的顺序出现,导致渲染图像错误,因为未考虑z-index

在此绘制中步骤中,主线程遍历布局树以创建绘制记录。绘制记录是绘制过程的注释。例如“先是背景,然后是文本,然后是矩形”。如果您使用canvas绘制了 元素,那么您可能会熟悉此过程。

  • 主线程遍历布局树并生成绘制记录

更新渲染管道的成本很高

在渲染管道中要掌握的最重要的事情是,在每个步骤中,先前操作的结果都用于创建新数据。例如,如果布局树中发生某些更改,则需要为文档收影响的部分重新生成绘制顺序。 如果要设置动画元素,浏览器必须在每帧之前运行这些操作,我们的大多数显示器每秒刷新屏幕60次;当您在每一帧的屏幕上移动物体时,动画将对人眼显得平滑。但是,如果动画错过了他们之间的帧,则页面将显示为混乱。

  • 时间线上的动画帧 即使渲染操作与屏幕刷新保持一致,这些计算仍在主线程上运行,这意味着在您的应用呈现运行javascript时可能会阻止它。
  • 时间轴上的动画帧,但JavaScript阻止了其中一帧 您可以将javascript操作分成小块,并计划在每个帧上运行requestAnimationFrame().您也可以在web worker中运行javascript,以避免阻塞主线程。
  • 在带有动画帧的时间轴上运行的JavaScript较小块

合成

您将如何绘制页面

现在,浏览器知道了文档的结构,每个元素的样式,页面的几何形状以及绘制顺序,它将如何绘制页面?将此信息转换为屏幕上的像素称为光栅化。

什么是合成

合成是一种将页面各个部分分成若干层,分别对其进行栅格化并在称为合成器的单独线程中作为页面进行合成的技术。如果发生滚动,则因为图层以及被栅格化,所以需要做的就是要合成一个新的帧。

分层几层

为了找出哪些元素需要位于哪个图层中,主线程遍历了布局树以创建图层树(在DevTools性能面板中,此部分称为“更新图层树”)。如果页面的某些部分应该是单独的层(如滑入式侧边菜单)没有得到,则可以使用will-changeCSS中的属性向浏览器提示。

  • 主线程遍历布局树生成层树 您可能很想为每个元素提供图层,但是与每帧对页面的一小部分栅格化相比,在过多数量的图层上进行合成可能会导致操作速度变慢,因此,衡量应用程序的渲染性能非常重要。

主线程的栅格和合成

一旦创建了层树并确定了绘制顺序,主线程便将该信息提交给合成线程。然后,合成器线程将每个图层栅格化。一层可能像页面整个长度一样大,因此合成器线程将他们划分为图块,并将每个图块发给栅格线程。栅格线程栅格化每个图块并将他们存储在GPU内存中。

  • 光栅线程创建图块的位图并发送到GPU 合成器线程可以优先处理不同的栅格线程,以便可以首先对视口(或附近)中的事物进行栅格化。图层还具有用于不同分辨率的多个拼贴,以处理诸如放大动作之类的事件。

栅格化后,合成器线程会收集称为“绘制四边形”的图块信息以创建一个合成器框架。

画四边形包含诸如图块在内存中的位置以及在考虑页面合成的情况下在图块中绘制图块的位置之类的信息。
合成器框架表示页面框架的四边形的集合
然后合成器框架通过IPC提交给浏览器进程。此时,可以从UI线程(用于更改浏览器UI)或从其他渲染器进程中添加另一个合成器框架。这些合成器帧被发送到GPU,以将其显示在屏幕上。如果发生滚动事件,则合成器线程会创建另一个合成器框架以发送到GPU。
  • 合成器线程创建合成框架。帧发送到浏览器进程,然后发送到GPU 合成的好处是它无需涉及主线程即可完成。合成器线程无需等待样式计算或javascript执行。这就是为什么仅合成动画被认为是获得最佳平滑效果的原因。如果需要重新计算布局或绘画,则必须涉及主线程。

第四部分

从浏览器角度的输入事件

当听到输入事件时,可能只想到在文本框中键入内容或单击鼠标,但从浏览器的角度来看,输入表示用户的任何手势。鼠标滚轮滚动是输入事件,触摸或鼠标悬停也是输入事件。

当发生用户手势(如屏幕上的触摸)时,浏览器进程就是首先接收手势的进程。但是,浏览器进程仅知该手势发生在哪里,因为选项卡内部的内容有渲染器进程处理。因此,浏览器进程将事件类型(如touchstart)及其坐标发送给渲染器进程。渲染器进程通过找到事件目标并运行附加的事件侦听器来适当的处理事件。

  • 输入事件通过浏览器进程路由到渲染器进程

合成器接收输入事件

在第三部分中,研究了合成器如何通过合成栅格化图层来平滑的处理滚动。如果页面上没有任何输入事件侦听器,则合成器线程可以创建一个完全独立于主线程的新复合框架。但是,如果将某些事件侦听器附加到页面上怎么办?合成器线程如何找出是否需要处理事件?

了解非快速滚动区域

由于javaScript是主线程的工作,因此在合成页面时,合成器线程会将页面上的具有事件处理程序的区域标记为非快速滚动区域。通过获取此信息,如果事件发生在该区域中,则合成器线程可以确保将输入事件发送到主线程。如果输入事件来自该区域之外,则合成器线程在不等待主线程的情况下进程帧的合成。

  • 描述非快速滚动区域

当编写事件处理程序时要注意

web开发中常见的事件处理模式是事件委托。由于事件冒泡,因此您可以在最顶层的元素上附加一个事件处理程序,并根据事件目标委派任务。例如:

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});

寻找活动目标

当合成器线程向主线程发生输入事件时,要运行的第一件事是名字测试目标,命中测试使用在渲染过程中生成的绘制记录数据来找到事件发生的点坐标下的内容。

最小化事件分配到主线程

在第三部分中,讨论了典型的显示如何每秒刷新屏幕60次,以及如何保持节奏以实现平滑动画。对于输入,典型的触摸屏设备每秒发送60-120次触摸事件,而典型的鼠标则每秒发送100次事件。输入事件的保真度高于我们屏幕刷新能力。 如果类似的连续事件touchmove每秒被发送到主线程120次,那么与屏幕的刷新速度相比,他可能会触发大量的点击事件和javascript执行。

  • 事件泛滥到框架时间轴上,导致页面变脏 为了尽量减少对主线程的过度调用,chrome的连续事件(如wheel,mousewheel,mousemove,pointermove,touchmove)调度延迟到下一个requestAnimationFrame。
  • 相比之前的时间表,事件合并并被延迟了

使用getCoalescedEvents得到帧内事件

对于大多数web应用程序,合并事件应该足以提供良好的用户体验。但是,如果要构建诸如绘制应用程序之类的东西并基于touchmove坐标放置路径,则可能会丢失中间的坐标以绘制平滑线。

  • 左侧的平滑触摸手势路径,右侧的合并受限路径
window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // 更具坐标进行画线
    }
});