7

引言

观察浏览器的任务管理器可以发现,打开浏览器的一个页面需要多个进程,包括浏览器进程、GPU 进程、网络进程、渲染进程等,有插件的话还会包括各种插件进程(Chrome 选项 -> 更多工具 -> 任务管理器)。

图片

本文将聚焦于浏览器的各个进程间是如何配合,将页面呈现给用户的。

📌 你将了解到

  • 浏览器在历史发展过程中,其进程架构做了哪些调整,为什么这样调整,以及解决了哪些问题?
  • 从用户在地址栏输入 URL,到页面渲染完成这之间发生了什么?回流和重绘是如何对浏览器性能造成影响的?

图片

1. 浏览器进程架构的演化

进程和线程

图片

进程

  • 一个进程就是一个程序的运行实例,它是由用来存放代码、运行中的数据以及一个执行任务的主线程的内存组成的运行环境;
  • 当一个进程关闭后,操作系统会回收为该进程分配的内存(即使该进程中存在因操作不当导致内存泄漏的线程);
  • 进程之间的内容是相互隔离的,这是为了保护操作系统中的进程互不干扰;
  • 当进程之间需要进行通信时,可使用进程间通信(IPC)机制。

线程

  • 线程是由进程来启动和管理的,一个应用程序在执行的时候会存在多个子任务的情况,使用多线程并行处理可以大大提升性能;
  • 由于线程依附于进程,进程中的任一线程执行出错也会导致整个进程的崩溃(因为内存是共享的);
  • 同一进程中的多个线程可共享进程所拥有的资源。这种资源包括内存空间,也包括操作系统的权限。
单进程和多进程浏览器

单进程浏览器

单进程浏览器是指所有功能模块(网络、插件、JS 运行环境、渲染引擎、页面等)都运行在同一进程中的浏览器(早期的 IE、Firefox)。

图片

单进程浏览器存在的问题:

  • 【不稳定】
    • 浏览器中的插件运行在浏览器的进程之中,插件的崩溃会引起整个浏览器的崩溃;
    • 渲染引擎通常也是不稳定的,例如复杂的 JS 脚本也会引起渲染引擎的崩溃,最终导致浏览器崩溃。
  • 【不流畅】
    • CPU 在某个时间点只能执行某个进程中的某一条线程。由于单进程浏览器中所有的页面的各种模块都在同一线程中运行,即同一时刻只能有一个模块可以执行。
    • 当一个页面的某个模块阻塞了该线程,就会导致整个浏览器失去响应;此外,页面的内存泄漏也会导致单进程浏览器使用时间越长,反应越慢。
  • 【不安全】
    • 线程共享进程资源,因而插件就能获取到浏览器运行过程中的数据,以及拥有和浏览器同等的系统权限。
    • 例如,插件可使用 C/C++ 编写,通过插件可以获取到操作系统任意资源;脚本也可以通过浏览器的漏洞来获取系统权限,引发安全问题。
多进程浏览器

Chrome 一问世便使用了多进程的架构,其页面运行在了单独的渲染进程中,插件运行在单独的插件进行中,进程间使用 IPC 进行通信。

浏览器的主要进程有哪些:

图片

  • 浏览器进程。相当于浏览器的大脑,主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中。
默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。因为渲染进程所有的内容都是通过网络获取的,会存在一些恶意代码利用浏览器漏洞对系统进行攻击,所以运行在渲染进程里面的代码是不被信任的。这也是为什么 Chrome 会让渲染进程运行在安全沙箱里,就是为了保证系统的安全。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,目前已独立出来,成为一个单独的进程。
  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
  • GPU 进程。当页面使用了硬件加速时,会使用它来渲染页面。
其实,Chrome 刚开始发布的时候是没有单独 GPU 进程的,都是放到浏览器主进程中的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

多进程浏览器是如何解决单进程浏览的问题的:

  • 【不稳定】正是由于进程之间相互隔离,当一个页面或者插件崩溃时只会影响当前的进程,不会影响到浏览器和其他页面。
  • 【不流畅】由于 JS 脚本运行在渲染进程中,即使 JS 阻塞了渲染进程,也只会影响当前页面的渲染,而其他页面的脚本则会运行在他们自己的渲染进程中,不受影响;此外,内存泄漏导致的不流畅问题也会随着一个页面的关闭导致一个进程的结束而解决。
  • 【不安全】多进程架构的安全沙箱,相当于是操作系统给进程上了一把锁,沙箱中的程序可运行不可写入、不可读取敏感数据。

多进程浏览器存在的问题:

  • 更高的资源占用。以 Chrome 浏览器为例,其将为每个页面分配单独的渲染进程,为每个插件分配单独的插件进程,因此会消耗更多内存资源。
  • 更复杂的体系架构。浏览器各个模块之间耦合度高、扩展性差目前的架构较难适应新需。

2. 导航流程

从用户发出 URL 请求到页面开始解析的过程,叫做导航,是网络加载流程和渲染流程之间的桥梁。

图片

  • 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 通过 IPC 转发给网络进程。
  • 然后,在网络进程中发起真正的 URL 请求。
  • 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
  • 浏览器进程接收到网络进程的响应头数据之后,发送 “提交文档 (CommitNavigation)” 消息到渲染进程。
  • 渲染进程接收到 “提交文档” 的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道。
  • 待网络进程中文档数据传输完成,渲染进程会向浏览器进程 “确认提交”,这是告诉浏览器进程:“已经准备好接收和解析页面数据了”。
  • 浏览器进程接收到渲染进程 “确认提交” 的消息之后,导航流程就结束了。此时,渲染进程就会开始解析页面和加载子资源了,浏览器进程将开始移除之前旧的文档,然后更新浏览器进程中的页面状态。

3. 渲染流程

渲染流水线

渲染流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局、分层、绘制、分块、光栅化和合成。

图片

构建 DOM 树(DOM)

浏览器无法直接理解和使用 HTML,所以要将其转化为浏览器能够理解的解构 —— 经过 HTML 解析器解析,输出树状结构的 DOM

图片

样式计算(Style)

目的是计算 DOM 节点中的每个元素具体样式,可分为三步

  • 渲染引擎把 CSS 文本转为浏览器可理解的结构 ——styleSheets 样式表
  • 标准化样式表中的属性值。这是由于渲染引擎无法理解 CSS 文本中的各种属性值,这些值会被转为标准化的计算值(例如 {color: blue} → {color: rgb(0, 0, 225)}、{font-weight: bold} → {font-weight: 700}
  • 计算出 DOM 树中每个节点的具体样式,计算过程遵守 CSS 的继承和层叠规则,被保存在 ComputedStyle 结构内

布局阶段(Layout)

计算 DOM 树中可见元素的几何位置信息,包括创建布局树和布局计算两个阶段

  • 创建布局树
    • 遍历 DOM 树中的所有需要渲染节点,并添加到布局树中
    • 不可见的节点如 head 标签下的全部内容,display: none 的标签等会被忽略

图片

  • 布局计算
    • 计算 DOM 节点的位置坐标,布局运算的结果会被写回布局树中

分层(Layer)

针对页面中的复杂效果,例如复杂的 3D 变换、页面滚动、z 轴排序等,渲染引擎将为特定节点生成专用的图层,并生成一颗图层树(Layer Tree)

图片

拥有层叠上下文属性的元素会被提升为单独的一层;需要剪裁的地方也会被创建为单独的图层

注意,并非布局树的每个节点都包含一个图层,一个节点可以直接或间接地属于一个层,例如一个节点可以从属于父节点的图层

图片

图层绘制(Paint)

渲染引擎会对图层树中每个图层进行绘制,将一个图层的绘制拆分成很多小的绘制指令,然后把这些指令按顺序组成一个待绘制列表

图片

栅格化(生成位图)

绘制列表准备好后,主线程将其提交给合成线程,实际的绘制操作由渲染引擎中的合成线程来完成

  • 合成线程会根据视口位置和大小,将图层(layer)划分为块(图块 tile)
  • 合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作由栅格化(将图块转换为位图)来执行,图块是栅格化的最小单位
  • 渲染进程会维护一个栅格化的线程池,栅格化过程通常都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫做快速栅格化,生成的位图被保存在 GPU 内存中

图片

合成与显示

  • 所有图块都被栅格化后,合成线程将生成绘制图块命令 DrawQuad 提交给浏览器进程
  • 浏览器进程中 viz 组件接收 DrawQuad 命令,根据此命令,将其页面内容绘制在内存中,最后再显示到屏幕上
流水线总结
  • 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
  • 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  • 创建布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。
  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  • 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

图片

回流和重绘

基于上述浏览器的渲染原理,我们可以理解回流和重绘是如何对浏览器性能造成影响的。由于浏览器渲染页面默认使用流式布局模型,当某个 DOM 或 CSS 几何属性发生改变后,文档流就会受到波动,就需要对 DOM 重新进行计算,重新布局页面,引发回流。

更新元素几何属性 —— 回流

  • 几何属性的修改会触发浏览器重新布局(Layout & Layer),渲染树需要重新生成,解析后来的一系列子阶段
  • 因此回流需要更新完整的渲染流水线,开销是最大的

图片

更新元素绘制属性 —— 重绘

  • 绘制属性的修改并没有导致几何位置的变化,所以不会导致布局阶段的执行,会直接进入绘制阶段,然后执行后来的子阶段
  • 重绘操作相比回流省去了布局和分层阶段,效率高于回流

图片

GPU 加速 —— 直接合成

  • 如果更改的属性不需要布局和绘制,渲染引擎会跳过布局和绘制,直接进入非主线程 —— 合成线程执行后续合成操作(比如利用 CSS3 的 transform、opacity、filter 这些属性就可以实现合成效果)
  • 例如,使用 CSS transform 实现动画效果的渲染流水线如下:一是避开了重绘、回流,因此避开了布局和绘制阶段;二是直接在非主线程执行合成动画操作,未占用主线程资源。相比于重绘和回流,合成大大提升了绘制效率

图片

Reference

  • 浏览器工作原理与实践
  • 浏览器进程架构的演化&version=13020110&nettype=WIFI&lang=zh_CN&fontScale=100&exportkey=n_ChQIAhIQGRRXFZ2dXQmFA2Bx5wQ4oBKAAgIE97dBBAEAAAAAAOFZNG6p7%2BUAAAAOpnltbLcz9gKNyK89dVj0RmL9G6zZTdJ1mZshRcBg33o4bJEPjfSX4iFReWCjilqk0NfpL0h1iqYKkX2DdW6yD0Adc4JldMIlENpdopQIarHLIcAACAIRLaSzIqVEbSy5JLdxC4wsdBcQ45Ql0oUR6jPwx7%2FIvQ40tphDDC3%2ByysOom29zfOo99XihFQ13nDQARASxLec1XBIK4vt6hKJwK1DVvZvNjCymwWdBeXRAG1acroj2axfwR4dzK161v9LTf9%2BMWeBD%2Bh%2FvEGKWvIRLLB0n8twV5OzxPdRqza4JjY5KY0kXWIpR7o%3D&acctmode=0&pass_ticket=NROenW8KktFdpx%2FehWWP2BcPvnem3EVXgzUh1htgRSOooPPCrE4swwsoZDu1C8jdABIA6JxLm7%2Ffo6AQWCBOmQ%3D%3D&wx_header=0&fontgear=2.000000)

关于本文
作者:@高扬
原文:https://mp.weixin.qq.com/s/6Q...


兰俊秋雨
5.1k 声望3.5k 粉丝

基于大前端端技术的一些探索反思总结及讨论