浏览器架构与渲染原理

注:以下内容来自于极客时间李兵老师的《浏览器工作原理与实践》课程总结

浏览器架构

多进程架构

概述

现代浏览器采用多进程架构的模式,一般而言,一个页面会是一个渲染进程(可能存在同一站点same-site多页面复用同一个渲染进程的情况),另外,还有浏览器的主进程、GPU进程、网络进程、插件进程等等。如下图
浏览器架构与渲染原理

各个进程的功能
  • 主进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
回顾单进程浏览器架构

单进程浏览器:浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。

架构图
浏览器架构与渲染原理
单进程架构的缺陷

  • 不稳定:多个线程运行在同一个进程中,尤其是插件线程的崩溃会导致整个进程的崩溃,复杂的JavaScript代码也容易导致页面线程的崩溃,进而导致整个进程的崩溃。所以,单进程架构不稳定。
  • 不流畅
    • 任务的排斥:所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的,所有页面在同一时刻只能有一个模块可以执行,一旦有耗时任务,则页面的其他任务无法执行,浏览器就会失去响应。
    • 内存泄漏:复杂的页面退出时,内存回收不完全,导致浏览器运行时间越长,会越来越卡顿。
  • 不安全:用C或C++撰写的插件可以获取操作系统的资源,另外,JavaScript脚本也可能获取到一定的系统权限,没有沙箱隔离,有很大的安全隐患。
多进程架构的特点
  • 解决了单进程架构的稳定性、流畅性和安全性相关的问题
  • 带来了更多的资源占用(如所有页面进程都包含有JavaScript执行引擎)
  • 带来了更复杂的体系架构(各模块之间的耦合度高,可扩展性降低)
未来:面向服务的架构(SOA)
  • 模块会被重构成独立的服务(Service)
  • 每个服务(Service)都可以在独立的进程中运行
  • 访问服务(Service)必须使用定义好的接口,通过 IPC 来通信
  • 构建一个更内聚、松耦合、易于维护和扩展的系统

架构如图
浏览器架构与渲染原理

浏览器渲染

从输入URL到页面显示的整个流程

浏览器架构与渲染原理

整个流程
  • 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。
  • 然后,在网络进程中发起真正的 URL 请求。
  • 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
  • 浏览器进程接收到网络进程的响应头数据之后,发送“提交导航 (CommitNavigation)”消息到渲染进程;
  • 渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道;
  • 最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。
  • 浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档(这里就会有白屏出现),然后更新浏览器进程中的页面状态。
导航

从用户输入URL到页面开始解析HTML之间的流程,称为导航。

  • 用户输入:当用户输入URL后,会触发当前页面的beforeunload事件。当前页面如果没有监听 beforeunload 事件或者同意了,则继续后续流程。此时标签页开始显示加载中的图标,同时地址栏也会出现X图标。
  • URL请求:浏览器网络进程开始与服务器通信,获取响应数据。通过对响应头的解析,如果不包含重定向之类的头部信息,则继续处理。如果包含重定向,则根据头部信息重新发出请求。
    • 响应数据类型的处理:浏览器根据头部Content-Type字段来判断是什么类型的响应数据。
      • text/html:HTML页面,继续导航流程,创建渲染进程。
      • application/octet-stream:字节流类型,浏览器会启用下载管理器来下载该类型文件,导航流程结束。
  • 准备渲染进程:默认情况下,Chrome 会为每个页面分配一个渲染进程
    • 同站点页面:浏览器会让多个页面直接运行在同一个渲染进程中。
      • 同一站点:定义为根域名(例如,geekbang.org)加上协议(例如,https:// 或者 http://),还包含了该根域名下的所有子域名和不同的端口。从同一站点的一个页面打开到另一个页面,则这两个页面复用一个渲染进程,称为process-per-site-instance。
  • 提交文档:浏览器进程将网络进程接收到的 HTML 数据提交给准备好的渲染进程。
    • 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
    • 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
    • 文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
    • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
  • 开始渲染阶段
渲染

浏览器架构与渲染原理

渲染流程很复杂,输入的HTML以及CSS、JavaScript最终经过渲染流程转化为像素显示出来。将渲染流程划分为一系列子阶段来剖析整个流程,即渲染流水线。
浏览器架构与渲染原理

渲染流水线构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化、合成 这些阶段。

  • 构建DOM树:经HTML解析器,将HTML文档转换为DOM树。
    浏览器架构与渲染原理
    在渲染引擎内部有一个HTML解析器,随着网络进程加载了多少数据,解析器就解析多少数据,而不是等文档整个加载完才解析。

    • DOM生成流程:字节流转换为DOM的过程
      浏览器架构与渲染原理

      • 分词器将字节流转换为Token
        浏览器架构与渲染原理

      • Token解析为DOM节点,将节点添加到DOM树。

    • 解析原理:HTML 解析器维护了一个 Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。

      具体的处理规则如下所示:

      • 如果压入到栈中的是 StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
      • 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
      • 如果分词器解析出来的是 EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
  • 样式计算

    • 把CSS纯文本转换为浏览器可以理解的stylesheets,即CSSOM。
    • 转换样式表中的属性值,使其标准化。一些属性值是文本、相对单位等等,都需要转换为数值和绝对单位,才能被浏览器理解。
    • 计算每个DOM节点的具体样式,利用了继承规则和层叠规则。
  • 布局阶段:计算DOM树中可见元素的几何位置。不可见元素不会被计算到布局中。

    • 创建布局树(Layout Tree)
      浏览器架构与渲染原理
      • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
      • 不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,比如属性包含 dispaly:none的元素
    • 布局计算
  • 分层:复杂的3D变换、页面滚动、z-index排序,都需要为特定的节点生成专用的图层。将这些图层叠加在一起构成最终的图像。

    • 从布局树中生成图层树(LayerTree)
      浏览器架构与渲染原理
      并不是布局树中的每个节点都包含一个图层,如果一个节点没有对应的层,则该节点从属于其父节点的图层。
    • 为节点单独创建图层的条件,满足以下两个条件之一即可
      • 拥有层叠上下文属性的元素会被提升为单独的一层。明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。
      • 需要剪裁(clip)的地方也会被创建为图层。比如,overflow属性的元素。
  • 图层绘制:渲染引擎把一个图层的绘制拆解为很多小的绘制指令,将这些指令按照顺序组成一个待绘制列表。
    浏览器架构与渲染原理

  • 分块:渲染进程的主线程将绘制列表提交给合成线程,接下来的流程由合成线程来执行。合成线程将图层划分为图块,然后按照视口附近的图块来优先生成位图。
    浏览器架构与渲染原理

    • 栅格化生成位图:将图块转换为位图。图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的栅格化都在线程池内执行。
      浏览器架构与渲染原理
      • 通常,栅格化都会使用GPU来加速生成,使用GPU生成位图的过程称为快速栅格化,位图保存在GPU内存中。这时候就会涉及到跨进程操作。
        浏览器架构与渲染原理
  • 合成和显示:所有图块都被光栅化之后,合成线程生成一个绘制图块的命令“DrawQuad”,然后提交命令给浏览器主进程。主进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上

重排与重绘

重排

更新了元素的几何属性,导致触发重新布局以及之后的一系列渲染子阶段都会执行,开销最大
浏览器架构与渲染原理

重绘

更新了元素的绘制属性,不会导致重新布局和分层,只会引起绘制阶段之后的渲染,开销较小。
浏览器架构与渲染原理

直接合成

更改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。如使用transforms 和 opacity属性来创建动画,目前合成器单独处理的属性只有这两个,因此坚持使用transform和opacity属性来创建CSS动画是提高绘制效率和浏览器流畅度的最佳实践。
另外,使用 will-change 或 translateZ 提升移动的元素也是最佳实践之一。
浏览器架构与渲染原理