浏览器工做原理

架构

进程和线程

进程能够被描述为是一个应用的执行程序。javascript

线程存在于进程并执行程序任意部分。css

启动应用时会建立一个进程。程序也许会建立一个或多个线程来帮助它工做,这是可选的。html

操做系统为进程提供了一个可使用的“一块”内存,全部应用程序状态都保存在该私有内存空间中。html5

关闭应用程序时,相应的进程也会消失,操做系统会释放内存。java

进程能够请求操做系统启动另外一个进程来执行不一样的任务。git

此时,内存中的不一样部分会分给新进程。若是两个进程须要对话,他们能够经过进程间通讯IPC)来进行。github

许多应用都是这样设计的,因此若是一个工做进程失去响应,该进程就能够在不中止应用程序不一样部分的其余进程运行的状况下从新启动。web

浏览器架构

那么如何经过进程和线程构建 web 浏览器呢?它可能由一个拥有不少线程的进程,或是一些经过 IPC 通讯的不一样线程的进程。算法

在2016年,Chrome官方团队使用“面向服务的架构”(Services Oriented Architecture,简称SOA)的思想设计了新的Chrome架构。浏览器

若是在资源受限的设备上(以下图),Chrome会将不少服务整合到一个进程中,从而节省内存占用。

进程 控制
浏览器进程 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
渲染进程 核心任务是将 HTML、CSS 和 JavaScript 转换为用户能够与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认状况下,Chrome会为每一个Tab标签建立一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
插件进程 主要是负责插件的运行,因插件易崩溃,因此须要经过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面形成影响。
网络进程 主要负责页面的网络资源加载,以前是做为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
GPU进程 在独立的进程中处理GPU任务。之因此放到独立的进程,是由于GPU要处理来自多个应用的请求,但要在同一个界面上绘制图形。

SOA架构的优势

  1. 稳定且流畅

    因为进程是相互隔离的,因此当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其余页面,这就完美地解决了页面或者插件的崩溃会致使整个浏览器崩溃,也就是不稳定的问题。

    同理,JavaScript也是运行在渲染进程中的,因此即便JavaScript阻塞了渲染进程,影响到的也只是当前的渲染页面,而并不会影响浏览器和其余页面。

  2. 安全沙箱

    SOA架构还有助于安全和隔离。由于操做系统有限制进程特权的机制,浏览器能够借此限制某些进程的能力。

    好比,Chrome会限制处理任意用户输入的渲染器进程,不让它任意访问文件。

  3. 更内聚,松耦合,易于维护和扩展

    原来的各类模块被重构成独立的服务(Service),每一个服务(Service)均可以在独立的进程中运行,访问服务(Service)必须使用定义好的接口,经过IPC来通讯,从而构建一个更内聚、松耦合、易于维护和扩展的系统,更好实现 Chrome 简单、稳定、高速、安全的目标。

站点隔离

站点隔离(Site Isolation for web developers)是新近引入Chrome的一个里程碑式特性,即每一个跨站点iframe都运行一个独立的渲染器进程。

即使像前面说的那样,每一个标签页单开一个渲染器进程,但容许跨站点的iframe运行在同一个渲染器进程中并共享内存空间,那安全攻击仍然有可能绕开同源策略,并且有人发如今现代CPU中,进程有可能读取任意内存( Meltdown/Spectre )。

进程隔离是隔离站点、确保上网安全最有效的方式。

Chrome 默认采用站点隔离。站点隔离是多年工程化努力的结果,它并不是多开几个渲染器进程那么简单。

好比,不一样的iframe运行在不一样进程中,开发工具在后台仍然要作到无缝切换,并且即使简单地Ctrl+F查找也会涉及在不一样进程中搜索。

导航

导航涉及浏览器进程与线程间为显示网页而通讯。一切从用户在浏览器中输入一个URL开始。输入URL以后,浏览器会经过互联网获取数据并显示网页。从请求网页到浏览器准备渲染网页的过程,叫作导航。

下面咱们逐步看一看导航的几个步骤。

第一步:处理输入

UI线程会判断用户输入的是查询字符串仍是URL。由于Chrome的地址栏同时也是搜索框。

第二步:开始导航

若是输入的是URL,首先,网络进程会查找本地缓存是否缓存了该资源。

若是有缓存资源,那么直接返回资源给浏览器进程。

若是在缓存中没有查找到资源,那么UI线程会通知网络线程发起网络调用,获取网站内容。

此时标签页左端显示旋转图标,网络线程进行DNS查询、创建TLS链接(对于HTTPS)。

网络线程可能收到服务器的重定向头部,如HTTP 301。此时网络线程会跟UI线程沟通,告诉它服务器要求重定向。而后,再发起对另外一个URL的请求。

第三步:读取响应

服务器返回的响应体到来以后,网络线程会检查接收到的前几个字节。响应的Content-Type头部应该包含数据类型,若是没有这个字段,则须要MIME类型嗅探

若是响应是HTML文件,那下一步就是把数据交给渲染器进程。但若是是一个zip文件或其余文件,那就意味着是一个下载请求,须要把数据传给下载管理器。

此时也是“安全浏览”检查的环节。若是域名和响应数据匹配已知的恶意网站,网络线程会显示警告页。

此外,CORB (Cross Origin Read Blocking) 检查也会执行,以确保敏感的跨站点数据不会发送给渲染器进程。

第四步:联系渲染器进程

全部查检完毕,网络线程确认浏览器能够导航到用户请求的网站,因而会通知UI线程数据已经准备好了。UI线程会联系渲染器进程渲染网页。

打开一个新页面采用的渲染进程策略:

  • 一般状况下,打开新的页面都会使用单独的渲染进程;
  • 若是从A页面打开B页面,且A和B都属于同一站点的话,那么B页面复用A页面的渲染进程;若是是其余状况,浏览器进程则会为B建立一个新的渲染进程。

    • 具体地讲,咱们将“同一站点”定义为根域名(例如,baidu.com)加上协议(例如,https:// 或者http://),还包含了该根域名下的全部子域名和不一样的端口。

      https://WWW.baidu.com
      https://WWW.baidu.com:8080
因为网络请求可能要花几百毫秒才能拿到响应,这里还会应用一个优化策略。第二步UI线程要求网络线程发送请求后,已经知道可能要导航到哪一个网站去了。所以在发送网络请求的同时,UI线程会提早联系或并行启动一个渲染器进程。这样在网络线程收到数据后,就已经有渲染器进程原地待命了。若是发生了重定向,这个待命进程可能用不上,而是换做其余进程去处理。

第五步:提交导航

数据和渲染器进程都有了,就能够经过IPC从浏览器进程向渲染器进程提交导航。渲染器进程也会同时接收到不间断的HTML数据流。

当浏览器进程收到渲染器进程的确认消息后,导航完成,文档加载阶段开始。

此时,地址栏会更新,安全指示图标和网站设置UI也会反映新页面的信息。

当前标签页面的会话历史会更新,后退/前进按钮起做用。为便于标签页/会话在关闭标签页或窗口后恢复,会话历史会写入磁盘。

最后一步:初始加载完成

提交导航以后,渲染器进程将负责加载资源和渲染页面(具体细节后面介绍)。

而在“完成”渲染后(在全部iframe中的onload事件触发且执行完成后),渲染器进程会经过IPC给浏览器进程发送一个消息。此时,UI线程中止标签页上的旋转图标。

初始加载完成后,客户端JavaScript仍然可能加载额外资源并从新渲染页面。

若是此时用户在地址又输入了其余URL呢?浏览器进程还会重复上述步骤,导航到新站点。不过在此以前,须要确认已渲染的网站是否关注beforeunload事件。由于标签页中的一切,包括JavaScript代码都由渲染器进程处理,因此浏览器进程必须与当前的渲染器进程确认后再导航到新站点。

若是导航请求来自当前渲染器进程(用户点击了连接或JavaScript运行了window.location = "https://newsite.com"),渲染器进程首先会检查beforeunload处理程序。而后,它会走一遍与浏览器进程触发导航一样的过程。惟一的区别在于导航请求是由渲染器进程提交给浏览器进程的。

导航到不一样的网站时,会有一个新的独立渲染器进程负责处理新导航,而老的渲染器进程要负责处理unload之类的事件。

更多细节,能够参考【译】页面生命周期API以及Web 页面生命周期

另外,导航阶段还可能涉及【中】Service Worker,即网页应用中的网络代理服务,开发者能够经过它控制什么缓存在本地,什么时候从网络获取新数据。

Service Worker说到底也是须要渲染器进程运行的JavaScript代码。若是网站注册了Server Worker,那么导航请求到来时,网络线程会根据URL将其匹配出来,此时UI线程就会联系一个渲染器进程来执行Service Worker的代码:可能只要从本地缓存读取数据,也可能须要发送网络请求。

若是Service Worker最终决定从网络请求数据,浏览器进程与渲染器进程间的这种往返通讯会致使延迟。

所以,这里会有一个“导航预加载”的优化Speed up Service Worker with Navigation Preloads,即在Service Worker启动同时预先加载资源,加载请求经过HTTP头部与服务器沟通,服务器决定是否彻底更新内容。

渲染

渲染是渲染器进程内部的工做,涉及Web性能的诸多方面(详细内容能够参考这里Why does speed matter?)。

标签页中的一切都由渲染器进程负责处理,其中主线程负责运行大多数客户端JavaScript代码,少许代码可能会由工做线程处理(若是用到了Web Worker或Service Worker)。

合成器(compositor)线程和栅格化(raster)线程负责高效、平滑地渲染页面。

渲染器进程的核心任务是把HTML、CSS和JavaScript转换成用户能够交互的网页接下来,咱们从总体上过一遍渲染器进程处理Web内容的各个阶段。

解析HTML

构建DOM

渲染器进程收到导航的提交消息后,开始接收HTML,其主线程开始解析文本字符串(HTML),并将它转换为DOM(Document Object Model,文档对象模型)。

DOM是浏览器内部对页面的表示,也是JavaScript与之交互的数据结构和API。

如何将HTML解析为DOM由HTML标准定义。HTML标准要求浏览器兼容错误的HTML写法,所以浏览器会“忍气吞声”,毫不报错。详情能够看看“解析器错误处理及怪异情形简介”。

加载子资源

网站都会用到图片、CSS和JavaScript等外部资源。浏览器须要从缓存或网络加载这些文件。主线程能够在解析并构建DOM的过程当中发现一个加载一个,但这样效率过低。

为此,Chrome会在解析同时并发运行“预加载扫描器”,当发现HTML文档中有<img><link>时,预加载扫描器会将请求提交给浏览器进程中的网络线程。

JavaScript可能阻塞解析

若是HTML解析器碰到<script>标签,会暂停解析HTML文档并加载、解析和执行JavaScript代码。

由于JavaScript有可能经过document.write()修改文档,进而改变DOM结构(HTML标准的“解析模型”有一张图能够一目了然)。因此HTML解析器必须停下来执行JavaScript,而后再恢复解析HTML。至于执行JavaScript的细节,你们能够关注V8团队相关的分享:【译】 JavaScript 引擎基础:Shapes 和 Inline Caches

提示浏览器你要加载资源

为了更好地加载资源,能够经过不少方式告诉浏览器。若是JavaScript没有用到document.write(),能够在<script>标签上添加asyncdefer属性。这样浏览器就会异步运行JavaScript代码,不会阻塞解析。合适的话,能够考虑使用JavaScript modules。再好比,<link rel="preload">告诉浏览器该资源对于当前导航绝对必要,应该尽快下载。关于资源加载优先级,能够参考这里:【译】Fast load times

计算样式(Recalculate style)

光有DOM还不行,由于并不知道页面应该长啥样。因此接下来,主线程要解析CSS并计算每一个DOM节点的样式。这个过程就是根据CSS选择符,肯定每一个元素要应用什么样式。在Chrome开发工具“计算的样式”(computed)中能够看每一个元素计算后的样式。

1.把CSS转换为浏览器可以理解的结构

CSS样式来源主要有三种:

  • 经过link引用的外部CSS文件;
  • <style>标记内的 CSS;
  • 元素的style属性内嵌的CSS。
  • 和HTML文件同样,浏览器也是没法直接理解这些纯文本的CSS样式,因此当渲染引擎接收到CSS文本时,会执行一个转换操做,将CSS文本转换为浏览器能够理解的结构——styleSheets
  • 为了加深理解,你能够在Chrome控制台中查看其结构,只须要在控制台中输入document.styleSheets,而后就看到以下图所示的结构, 该数据结构同时具有了查询和修改功能。

2.转换样式表中的属性值,使其标准化

3.计算出DOM树中每一个节点的具体样式

这就涉及到CSS的继承规则和层叠规则了。

首先是CSS继承。

  • 首先,能够选择要查看的元素的样式(位于图中的区域2中),在图中的第1个区域中点击对应的元素元素,就能够了下面的区域查看该元素的样式了。好比这里咱们选择的元素是<p>标签,位于html.body.div.这个路径下面。
  • 其次,能够从样式来源(位于图中的区域3中)中查看样式的具体来源信息,看看是来源于样式文件,仍是来源于UserAgent样式表。这里须要特别提下UserAgent样式,它是浏览器提供的一组默认样式,若是你不提供任何样式,默认使用的就是UserAgent样式。
  • 最后,能够经过区域2和区域3来查看样式继承的具体过程。

样式计算过程当中的第二个规则是样式层叠。层叠是CSS的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法

布局(Layout)

到这一步,渲染器进程知道了文档的结构,也知道了每一个节点的样式。但基于这些信息仍然不足以渲染页面。

布局,就是要找到元素间的几何位置关系。主线程会遍历DOM元素及其计算样式,而后构造一棵布局树,这棵树的每一个节点将带有坐标和大小信息。

布局树与DOM树的结构相似,但只包含页面中可见元素的信息。若是元素被应用了display: none,则布局树中不会包含它(visibility: hidden的元素会包含在内)。相似地,经过伪类p::before{content: 'Hi!'}添加的内容会包含在布局树中,但DOM树中却没有。

为了构建布局树,浏览器大致上完成了下面这些工做:

  • 遍历DOM树中的全部可见节点,并把这些节点加到布局中;
  • 而不可见的节点会被布局树忽略掉,如head标签下面的所有内容,再好比body.p.span这个元素,由于它的属性包含 dispaly:none,因此这个元素也没有被包进布局树。

肯定页面的布局要考虑不少不少因素,并不简单。好比,字体大小、文本换行都会影响段落的形状,进而影响后续段落的布局。CSS可以让元素浮动到一边、隐藏溢出边界的内容、改变文本显示方向。可想而知,布局阶段的任务是很是艰巨的。Chrome有一个工程师团队专司布局,感兴起的话,能够看看他们这个分享:BlinkOn 8: Block Layout Deep Dive(在YouTube上)。

更新了元素的几何属性(重排)

从上图能够看出,若是你经过JavaScript或者CSS修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发从新布局,解析以后的一系列子阶段,这个过程就叫重排。无疑,重排须要更新完整的渲染流水线,因此开销也是最大的

分层(Layer)

由于页面中有不少复杂的效果,如一些复杂的3D变换、页面滚动,或者使用z-indexing作z轴排序等,为了更加方便地实现这些效果,渲染引擎还须要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

要想直观地理解什么是图层,你能够打开Chrome的“开发者工具”,选择“Layers”标签(开发者工具 -> More tools ->Layers),就能够可视化页面的分层状况。

一般状况下,并非布局树的每一个节点都包含一个图层,若是一个节点没有对应的层,那么这个节点就从属于父节点的图层。

那么须要知足什么条件,渲染引擎才会为特定的节点建立新的层呢?

第一点,拥有层叠上下文属性的元素会被提高为单独的一层。

从图中能够看出,明肯定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性。

第二点,须要剪裁(clip)的地方也会被建立为图层。

<style>
      div {
            width: 200;
            height: 200;
            overflow:auto;
            background: gray;
        } 
</style>
<body>
    <div >
        <p>因此元素有了层叠上下文的属性或者须要被剪裁,那么就会被提高成为单独一层,你能够参看下图:</p>
        <p>从上图咱们能够看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一块儿也是一颗树状结构。</p>
        <p>图层树是基于布局树来建立的,为了找出哪些元素须要在哪些层中,渲染引擎会遍历布局树来建立层树(Update LayerTree)。</p> 
    </div>
</body>

在这里咱们把div的大小限定为200 200像素,而div里面的文字内容比较多,文字所显示的区域确定会超出200 200的面积,这时候就产生了剪裁。

出现这种裁剪状况的时候,渲染引擎会为文字部分单首创建一个层,若是出现滚动条,滚动条也会被提高为单独的层。

若是页面某些部分应该独立一层(如滑入的菜单)但却没有,那你能够在CSS中给它加上will-change属性来提醒浏览器。

分层并非越多越好,合成过多的层有可能还不如每帧都对页面中的一小部分执行一次栅格化更快。关于这里边的权衡,能够参考:坚持仅合成器的属性和管理层计数

图层绘制(Paint)

在完成图层树的构建以后,渲染引擎会对图层树中的每一个图层进行绘制,那么接下来咱们看看渲染引擎是怎么实现图层绘制的?

渲染引擎实现图层的绘制,会把一个图层的绘制拆分红不少小的绘制指令,而后再把这些指令按照顺序组成一个待绘制列表,以下图所示:

从图中能够看出,绘制列表中的指令其实很是简单,就是让其执行一个简单的绘制操做,好比绘制粉色矩形或者黑色的线等。

而绘制一个元素一般须要好几条绘制指令,由于每一个元素的背景、前景、边框都须要单独的指令去绘制。因此在图层绘制阶段,输出的内容就是这些待绘制列表

更新元素的绘制属性(重绘)

从图中能够看出,若是修改了元素的背景颜色,那么布局阶段将不会被执行,由于并无引发几何位置的变换,因此就直接进入了绘制阶段,而后执行以后的一系列子阶段,这个过程就叫重绘。相较于重排操做,重绘省去了布局和分层阶段,因此执行效率会比重排操做要高一些

栅格化(raster)

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操做是由渲染引擎中的合成线程来完成的。

当图层的绘制列表准备好以后,主线程会把该绘制列表提交(commit)给合成线程

  • 一般一个页面可能很大,可是用户只能看到其中的一部分,咱们把用户能够看到的这个部分叫作视口(viewport)。
  • 有些状况下,图层很大,可是经过视口,用户只能看到页面的很小一部分。因此这种状况下,要绘制出全部图层内容的话,没有必要。

    基于这个缘由,合成线程会将图层划分为图块(tile),这些图块的大小一般都是256 x 256 或者 512 x 512。

合成器线程会安排栅格化线程优先转换视口(及附近)的图块。而构成一层的图块也会转换为不一样分辨率的版本,以便在用户缩放时使用。

所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,全部的图块栅格化都是在线程池内执行的。

一般,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。

相信你还记得,GPU操做是运行在GPU进程中,若是栅格化操做使用了GPU,那么最终生成位图的操做是在GPU中完成的,这就涉及到了跨进程操做。

合成

什么是合成?合成(composite)是将页面不一样部分先分层并分别栅格化,而后再经过独立的合成器线程合成页面。

这样当用户滚动页面时,由于层都已经栅格化,因此浏览器惟一要作的就是合成一个新的帧。而动画也能够用一样的方式实现:先移动层,再合成帧。

全部小片都栅格化之后,合成器线程会收集叫作“绘制方块”(draw quad)的图块信息,以建立合成器帧。

  • 绘制方块:包含小片的内存地址、页面位置等合成页面相关的信息。
  • 合成器帧:由从多绘制方块拼成的页面中的一帧。

建立好的合成器帧会经过IPC提交给浏览器进程。浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,而后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

与此同时,为更新浏览器界面,UI线程可能还会添加另外一个合成器帧;或者由于有扩展,其余渲染器进程也可能添加额外的合成器帧。

全部这些合成器帧都会发送给GPU,以便最终显示在屏幕上。若是发生滚动事件,合成器线程会再建立新的合成器帧并发送给GPU。

使用合成的好处是不用牵扯主线程。合成器线程不用等待样式计算或JavaScript执行。

这也是为何“只需合成的动画”(【中】High Performance Animations)被认为性能最佳的缘由。由于若是布局和绘制须要再次计算,那还得用到主线程。

用一张图来展现:

渲染流水线大总结

好了,咱们如今已经分析完了整个渲染流程,从HTML到DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程:

  1. 渲染进程将HTML内容转换为可以读懂的DOM树结构。
  2. 渲染引擎将CSS样式表转化为浏览器能够理解的styleSheets,计算出DOM节点的样式。
  3. 建立布局树,并计算元素的布局信息。
  4. 对布局树进行分层,并生成分层树。
  5. 为每一个图层生成绘制列表,并将其提交到合成线程。
  6. 合成线程将图层分红图块,并在光栅化线程池中将图块转换成位图。
  7. 合成线程发送绘制图块命令DrawQuad给浏览器进程。
  8. 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。

参考:

深刻理解现代浏览器

浏览器工做原理与实践

[[译] 现代浏览器内部揭秘(第一部分)](https://juejin.cn/post/684490...