宏观下的浏览器

356 阅读11分钟

一个古老的面试问题:当你在浏览器中输入 google.com 并且按下回车之后发生了什么?

简介

浏览器发展史

1991 年 Tim Berners-Lee 发布首代浏览器 WorldWideWeb(万维网),后改名为Nexus。

1993 年 NCSA 开发了 Mosaic 多平台浏览器,并且授权予多间公司,让它们创造自己的产品。

1994 年 Mosaic 的开发者之一 Marc Andreessen 成立了网景通信公司,开发了 Mosaic Netscape 浏览器(网景导航者)。发布之后占领了绝大多数市场份额。

1995 年 微软在获取到 Mosaic 授权之后,开发了 IE 1.0/2.0,与 windows 95 捆绑。

1996 年 微软发布 IE 3.0 成为第一款支持编程语言及CSS的商用浏览器,使市场占有率开始紧追网景,此时网景市场份额 86%。

1998 年 网景开放了源代码,命名为 Mozilla。

1999 年 IE 因为在 windows 的捆绑下,IE 市场份额达到 99%。

2003 年 苹果发布了 Safari 浏览器,与苹果操作系统捆绑。

2004 年 基于 Mozilla 推出了 FireFox 1.0。

2005 年 苹果开源了 Safari 浏览器内核 webkit。同年,因为性能和体验的问题,微软放弃 IE,推出基于 webkit 的 IE 替代品 Edge。

2008 年 谷歌基于 webkit 开发了 chromium,在 chromium 基础上发布了 chrome。

根据 StatCounter 统计,截止到2021年06月,浏览器的市场占有率中 Chrome 占 65.27%,IE 目前仅占 0.61%,可以看出 IE 已经即将推出浏览器的历史舞台,作为 IE 的升级版 Edge 也只占有市场的 3.4%。

image.png

浏览器的高层结构

image.png

浏览器的抽象分层结构图中将浏览器分成了以下几个子系统:

  • 用户界面(User Interface) 除了标签页内容窗口之外的其他用户界面内容。包括地址栏、前进后退按钮等。image.png
  • 浏览器引擎(Browser Engine) 用于在用户界面和渲染引擎之间传递数据。
  • 渲染引擎(Rendering Engine) 负责渲染用户请求的页面内容。
  • 网络(Networking) 渲染引擎下的功能模块,负责网络请求。
  • JavaScript 解释器(JavaScript Interpreter) 渲染引擎下的功能模块,用于解析和执行 JS。
  • 数据持久层(Data Persistence) 帮助浏览器存储各种数据,比如 cookie 等。

其中渲染引擎常常被称为浏览器的内核,不同浏览器使用的内核不相同:

IE 使用的是 Trident 内核,Firefox 使用的是 Gecko 内核,Safari 使用的是 Webkit 内核,Chrome/Opera/Edge 使用的是 Blink 内核(2013 年推出)。

浏览器的进程架构

进程和线程

这里简单了解下进程线程的概念。

image.png

当我们在启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存空间里。

当应用关闭时,该内存空间就会被回收。

进程可以请求操作系统创建了多个进程,每个进程之间的内存空间相互独立。如果两个进程之间需要相互传递数据,则需要通过进程间通信管道 IPC(Inter Process Communication)来传递。

image.png

进程可以将任务分成很多细小的任务,然后通过创建多个线程并行执行不同的任务。同一进程下的线程是可以直接通信共享数据的。

早期的单进程浏览器

image.png

所有功能运行在一个进程里,单进程结构会出现很多问题:

  • 不稳定
    其中一个线程卡死就会导致整个进程出问题。比如:打开一个标签页卡死,导致整个浏览器无法运行。
  • 不安全
    因为整个浏览器是在一个进程中,所以浏览器之间是可以共享数据的,那 JS 引擎就可以随便访问内存中的共享数据。
  • 不流畅
    一个线程需要负责太多事情,就会导致运行效率的问题。

所以为了解决以上问题,现在采用多进程浏览器结构。

多进程浏览器架构 image.png

  • 浏览器进程
    控制浏览器用户界面内功能,包括地址栏、前进后退按钮等。
    负责协调浏览器的其他进程。
  • 渲染进程/备用渲染进程
    负责控制 Tab 标签页的显示内容。
    渲染进程都运行在沙箱模式下。
    通常情况下浏览器会为每个标签页创建一个渲染进程,这取决于浏览器采用的进程模型。
  • GPU 进程
    负责整个浏览器界面的渲染。
  • 插件进程
    负责控制浏览器内使用的所有插件。
  • 网络进程
    负责发起接收网络请求。
  • 缓存进程、扩展程序进程等等

浏览器提供了 4 种进程模型。

  • Single process
    表示让浏览器引擎和渲染引擎使用同一进程。

  • Process-per-tab
    表示一个 Tab 里的所有站点使用同一进程。

  • Process-per-site
    表示同一站点使用同一进程。

  • Process-per-site-instance(默认的)
    表示访问不同站点和同一站点的不同页面都会创建新的进程。
    但是如果从一个页面打开了新页面,而新页面和当前页面属于同一站点时,那么新页面会复用父页面的渲染进程。
    image.png

导航过程

1. 处理输入

当你在浏览器地址栏输入地址时,浏览器进程中的 UI 线程会捕捉你的输入内容,判断内容是搜索查询还是 URL。

image.png

当您试图从一个站点切换到新站点时,beforeunload 可以创建“离开此站点?”警告。所以当新的导航请求传入时,浏览器进程必须检查当前的渲染进程

image.png

当新导航是新站点时,将调用新的渲染进程,保留老的渲染进程用来处理卸载等事件。

image.png

2. 网络请求

如果是搜索查询,则将关键词发送给搜索引擎。

如果是 URL,当用户点击回车时,UI 线程会启动一个网络线程调用来获取相应的内容。

image.png

网络通讯的相关内容,不在此详细说明。

3. 解析响应

当网络线程获取到数据后,会通过 SafeBrowsing (谷歌内部的站点安全系统)来检查站点是否是恶意站点。SafeBrowsing 通过检测该站点的数据来检测是否安全,比如:检查该站点的 IP 是否在谷歌的黑名单。

如果是,则会提示警告页面,浏览器会阻止你的访问,当然也可以继续访问。

image.png

网络进程解析 HTTP 出来的响应头数据,并将其转发给浏览器进程。

浏览器进程根据返回的响应头 content-type 来判断数据类型。如果是 html 文件,则将数据传递给渲染器进程。如果是其他文件等下载请求,则交给下载管理器。

4. 准备渲染进程

当返回数据准备完毕,并且通过安全校验时,网络线程会通知 UI 线程我准备好了,然后 UI 线程会创建一个渲染器进程来渲染页面。

网络进程和 UI 线程创建渲染器进程的过程是并行的,当网络线程接收到数据时,渲染进程已经进入备用阶段。

image.png

5. 提交导航

现在响应头数据和渲染进程都准备好了,浏览器进程向渲染进程发送一个IPC以提交导航。

渲染进程接收到消息之后,便开始准备接收HTML数据,接收数据的方式是直接和网络进程建立数据管道。

image.png

当渲染进程接收完数据之后,便向浏览器进程确认提交,一旦浏览器进程听到提交在呈现程序进程中发生的确认,导航就完成了,文档加载阶段就开始了。

这时浏览器进程就会开始更新自己的 ui 界面状态,包括地址栏、前进后退的历史状态等。

渲染流程

渲染器进程接收到的数据也就是 html,渲染器进程的核心任务就是把 html、css、js、image 等资源渲染成用户可以交互的 web 页面。

image.png

1. 构建 DOM 树

当渲染进程接收到导航的提交信息以及接收到 html 数据时,渲染器进程的主线程开始将 html 进行解析,构造 DOM 数据结构,DOM(Document Object Model) 即浏览器对页面在其内部的表现形式,是一种可以交互的数据结构和 API。

image.png

html 解析算法 由两个阶段组成,标记化和树构建。

首先经过 Tokenniser 标记,这是词法分析的过程,将输入内容解析成多个标记,html 标记包括起始标记、结束标记、属性名称和属性值。

根据识别后的标记进行 DOM 树构造,在 DOM 树构造过程中会创建 Document 对象,然后以 Document 为根结点的 DOM 树不断进行修改,向其中添加各种元素。

image.png

html 代码中会引用一些额外的资源,比如图片、CSS等,这些资源需要通过网络下载或者缓存中加载,这些资源不会阻塞 html 的解析,因为他们不会影响 DOM 的生成。

“预加载扫描器” 会并发运行。如果HTML文档中有像或这样的东西,预加载扫描器会偷看HTML解析器生成的标记,并在浏览器进程中向网络线程发送请求。

但当 html 标签解析过程中遇到 script 标签时,就会停止 html 的解析流程,转而去加载解析并且执行 JS,因为 JS 脚本有可能会改变当前页面的 html 结构。所以说 script 标签要放在合适的位置,或者配置异步加载。

例如:

<html>
  <body>
    content
    <script>
      document.write("--foo")
    </script>
  </body>
</html>

那么DOM解析器会先执行JavaScript脚本,执行完成之后,再继续往下解析。

<html>
  <body>
    content
    <script type="text/javascript" src="foo.js"></script>
  </body>
</html>

这种情况下,当解析到JavaScript的时候,会先暂停DOM解析,并下载foo.js文件,下载完成之后执行该段JS文件,然后再继续往下解析DOM。这就是JavaScript文件为什么会阻塞DOM渲染。

<html>
  <head>
    <style type="text/css" src = "theme.css"></style>
  </head>
  <body>
    <p>content</p>
    <script>
      let e = document.getElementsByTagName('p')[0]
      e.style.color = 'blue'
    </script>
  </body>
</html>

当我在JavaScript中访问了某个元素的样式,那么这时候就需要等待这个样式被下载完成才能继续往下执行,所以在这种情况下,CSS也会阻塞DOM的解析。

2. 样式计算

在 html 解析完成后,我们就会获得一个 DOM 树,但我们还不知道 DOM 树上的节点的样式。

主线程需要解析 CSS 并确定每个 DOM 节点的计算样式。

image.png

  1. 解析 CSS 将 CSS 解析成浏览器能够理解的结构 styleSheets。

image.png

  1. 转换属性值,使其标准化

这一步主要对属性值进行标准化操作,需要将所有值转换为渲染引擎容易理解的、标准化的计算值,比如 2em 转换为 32px,red 转换为 rgb(255,0,0),bold 转换为 700 等。

  1. 计算出每个节点的计算样式

除了自定义样式之外,浏览器有内置的默认样式表

image.png

3. 布局阶段

现在渲染进程已经拿到了完整的 DOM 树和每个节点的样式,但是这不足以绘制出完整的页面。

image.png

布局是一个寻找元素几何形状的过程。主线程遍历 DOM 和计算样式,并创建布局树,其中包含 xy 坐标和边框大小等信息。

布局树的结构可能与 DOM 树类似,但它只包含与页面上可见内容相关的信息

如果应用了​display: none​,则该元素不是布局树的一部分 (然而,具有 ​visibility: hidden​ 的元素在布局树中)。

类似地,如果一个伪类在 ​{ content:"Hi! }​,它被包含在布局树中,即使它不在DOM中。

image.png

4. 分层

主线程遍历布局树生成层树。

image.png

为什么不将每个元素分为一层?因为会影响后面跨过多层进行合成是的效率。

那如何去判断是否提升为新图层呢?

  1. 拥有层叠上下文属性的元素会被单独提升为一层\

  2. 需要裁剪的地方也会创建为图层

5. 图层绘制

主线程遍历各个图层,生成绘制指令。

6. 栅格化操作

主线程将绘制指令发送给合成线程。

合成线程将图层分成图块,并将每个图块发送给栅格线程,光栅线程光栅化每个贴图并将它们存储在 GPU 内存中,当然会优先光栅视口附近的图块。

image.png

7. 显示

一旦所有的图块都被光栅化完成,合成线程就会发送 DrawQuad 给浏览器进程,浏览器就可以接收到渲染进程的输出,显示到页面上了。

image.png

参考资料

  1. developers.google.com/web/updates…
  2. www.html5rocks.com/zh/tutorial…
  3. html.spec.whatwg.org/multipage/p…
  4. www.chromium.org/developers/…