浏览器架构、渲染原理与页面优化

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

浏览器架构

多进程架构

概述

现代浏览器采用多进程架构的模式,一般而言,一个页面会是一个渲染进程(可能存在同一站点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 来通信
  • 构建一个更内聚、松耦合、易于维护和扩展的系统

架构如图
SOA架构图

浏览器渲染

从输入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树。
    DOM树构建
    在渲染引擎内部有一个HTML解析器,随着网络进程加载了多少数据,解析器就解析多少数据,而不是等文档整个加载完才解析。

    • DOM生成流程:字节流转换为DOM的过程
      DOM生成流程

      • 分词器将字节流转换为Token
        字节流转换为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 元素解析完成。
    • 解析阻塞:在解析HTML构建DOM树时,如果遇到<script>标签,则会阻塞解析,加载JavaScript文件,而JavaScript文件的执行又会依赖于CSSOM,因此在执行JavaScript时,CSS文件的加载也会阻塞解析,必须等到CSS文件加载完才能执行JavaScript。

    • 预解析:为了优化性能,Chrome会开启一个预解析操作,渲染引擎会开启一个预解析线程,将HTML文件中的JavaScript、CSS等相关外部资源解析后提前下载,加快页面的加载速度。

    • 异步JavaScript文件:如果JavaScript文件中没有DOM操作,可以将JavaScript脚本设置为异步加载(defer或async),避免阻塞DOM树构建。

      • async标记的JavaScript脚本会在加载后立即执行
      • defer标记的JavaScript脚本则会在DOMContentLoaded事件之前执行
  • 样式计算

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

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

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

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

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

白屏优化

浏览器在移除旧文档、解析新文档还未完成首次渲染的过程,整个页面为白屏,白屏的时间如果太长,会很影响用户体验。

白屏的瓶颈:
  • 下载CSS、JavaScript文件
  • 执行JavaScript文件
白屏的优化
  • 内联CSS、JavaScript,从而移除这两种文件的下载。
  • 减少文件的大小
  • 给JavaScript使用异步加载模式
  • 使用媒体查询,加载特定媒体类型的CSS文件
显示器显示图像

以刷新率为60Hz为例,显示器会每秒读取60次显卡的前缓冲区中的图像,然后显示出来。

显卡

显卡的职责就是合成新的图像,将图像保存到后缓冲区,然后系统让后缓冲区与前缓冲区互换,这样就保证了显示器读取到最新的图像。
通常,显卡的更新频率与显示器的刷新频率一致,但在一些复杂场景下,显卡处理的频率变慢时,就会造成视觉上的卡顿现象。

分层和合成机制
  • 分层与合成机制是浏览器实现高效渲染的关键机制。因为合成不在主线程进行,所以避免了主线程上的很多渲染流程。
    • 分层可以使得独立分层的元素的各种变换都在自己的图层中进行,不会影响到其他图层的元素。
    • 合成可以使得最终的图片是各图层叠加后的结果。合成操作是在合成线程上执行的,因此有的时候主线程虽然卡顿,但CSS动画依然可以流畅执行。
  • 分块机制:图层被分为大小固定的图块,优先绘制靠近视口区域的图块,而不是将图层完全绘制出来再合成。
    • 纹理上传:从计算机内存上传到GPU内存的过程,这是影响分块机制效率的关键。纹理上传耗时长,因此Chrome采用了首次合成图块时使用低分辨率的图片的策略,这样纹理上传的内容减少了很多,然后合成器再绘制完成正常分辨率的图片后再替换低分辨率图片。
重排与重绘
重排

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

重绘

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

直接合成

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

页面优化

页面优化是为了更快地显示与响应用户行为。因此,优化主要集中在页面的加载与交互阶段

加载阶段的优化

加载阶段渲染流水线
阻塞页面的首次渲染主要是HTML资源、CSS资源和JavaScript资源。这些资源称为关键资源,而其他多媒体资源则是非关键资源。优化的目标就是这些关键资源。

关键资源

影响页面首次渲染的因素

  • 关键资源的个数:关键资源的个数越多,首次渲染的加载时间越长
  • 关键资源的大小:关键资源的大小越大,首次渲染的加载时间越长
  • 关键资源的RTT:请求关键资源所花费的往返时延越长,首次渲染的加载时间越长。
针对关键资源的优化原则

减少关键资源的个数,降低关键资源的大小,降低关键资源的RTT

  • 内联样式和脚本
  • 脚本使用defer或async来异步加载,避免阻塞
  • 样式表在外链时,使用媒体类型和媒体查询来标记为非阻塞样式<link rel="stylesheet" href="style.css" media="min-width:360px">
    • CSS默认是阻塞的,称为阻止内容呈现的CSS。浏览器会下载所有的CSS资源,只不过非阻塞的优先级低而已。换句话说,就算当前设备的媒体类型不符合media属性,也会下载该CSS资源,只是优先级比其他资源低。
  • 压缩关键资源
  • 使用CDN减少RTT
交互阶段的优化

交互阶段渲染流水线
触发生成帧

  • 由JavaScript通过修改DOM或CSSOM,造成重排或重绘
  • 由CSS触发,一般是一些CSS特效或动画,不触发重排或重绘,而是直接合成,效率高
优化原则

让单个帧的生成速度变快

优化方式
  • 减少JavaScript的执行时间:让主线程有时间去执行其他渲染工作,加快单个帧的生成

    • 将任务分解为多个小任务执行
    • 采用web workder来运行计算繁重的任务(web workder是额外的工作线程,没有DOM、CSSOM环境)
  • 避免强制同步布局:

    • 正常布局操作:浏览器会将所有的DOM修改操作存储到一个队列中,直到满足了一定条件(一定时间后或队列数量达到阈值),批量应用修改到DOM上。因此,JavaScript对DOM的修改并不一定立刻生效。
    • 强制同步布局:JavaScript强制浏览器做同步布局,也就是JavaScript修改完DOM后,立刻让浏览器执行DOM重排。
      • 触发强制同步布局的情况:在修改DOM后访问元素的几何属性时会触发,如宽高、几何位置、获取计算后样式getComputedStyle等等(具体参照该网站

        function logBoxHeight() {
        
          box.classList.add('super-big');
        
          // Gets the height of the box in pixels
          // and logs it out.
          console.log(box.offsetHeight);
        }
        
      • 避免触发强制布局:将访问元素几何属性放在修改DOM之前,这时候访问到的是上一帧的几何属性。大多数情况下,读取上一帧的布局信息足够了。

        function logBoxHeight() {
          // Gets the height of the box in pixels
          // and logs it out.
          console.log(box.offsetHeight);
        
          box.classList.add('super-big');
        }
        
  • 避免布局抖动:

    • 布局抖动:连续地强制同步布局,如在一个循环语句中,每次循环都去访问几何属性,然后修改DOM,这样浏览器在每次循环中都必须去强制执行同步布局,造成生成帧的速度变慢。

      function resizeAllParagraphsToMatchBlockWidth() {
      
        // Puts the browser into a read-write-read-write cycle.
        for (var i = 0; i < paragraphs.length; i++) {
          paragraphs[i].style.width = box.offsetWidth + 'px';
        }
      }
      
    • 避免布局抖动:将需要访问的几何属性值存储下来,在每次循环中使用存储的值来修改DOM即可。

      // Read.
      var width = box.offsetWidth;
      
      function resizeAllParagraphsToMatchBlockWidth() {
        for (var i = 0; i < paragraphs.length; i++) {
          // Now write.
          paragraphs[i].style.width = width + 'px';
        }
      }
      
  • 合理使用CSS合成动画

    • 使用transform和opacity构建CSS动画
    • 使用will-change来告知渲染引擎,不过will-change是浏览器执行优化的最后手段,不到万不得已没必要使用,这会同时增加内存消耗。
  • 避免频繁的垃圾回收:避免在函数中创建大量的临时对象,防止垃圾回收机制频繁运行。优化存储结构,避免小颗粒对象的生成。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值