谷歌浏览器网站性能优化 (移动端浏览器性能优化探索)

作者:鲁田(程禄)

在移动端的页面开发过程中,经常提及页面性能优化、消除页面卡顿的话题,如何确定优化策略,我们首先应当对页面卡顿的行为有所认知。

一、前言

页面的卡顿现象可以比较明确的分为三个类型,分别是“画面撕裂”、“丢帧不流畅”、“长时间未响应”。

“画面撕裂”现象给人直观的感觉是页面返回内容不一致,而造成这种现象的原因在于屏幕的刷新机制,屏幕的刷新遵循“Z”字刷新的方式,因此同一帧的数据在上屏时存在一定的时间差,当帧率大于刷新率时,屏幕对前一帧的数据上屏尚未结束而后台对后一帧的数据处理合成完毕,此时屏幕完成上屏的数据将会采用后一帧的数据,造成人眼同一时刻观测的画面数据来源于不同帧的现象,给人以撕裂感。

”画面丢帧“也被称为Jank现象,给人的直观感觉是画面不流畅,在动画或滚动时出现短暂停滞,而造成丢帧的原因在于屏幕每隔16ms发出一个VSync信号,在正常帧的流程中,CPU接收到VSync信号后会先通过交换指针的方式进行前后缓冲区的数据同步(该过程时耗忽略不计)然后开始新一帧数据合成,当帧率小于刷新率时,下一个VSync信号到来时可用于交换的帧数据还没有通过CPU 计算合成,此时前后缓冲区的数据不会发生交换,因此屏幕内出现了连续两帧使用同一帧数据渲染,给人以停顿和不流畅的感觉。

"长时间未响应"指的是画面长时间等待,等待网络请求或其他事件处理,这种情况通常并不是由于帧渲染导致的,但等待时长如果违背了RAIL模型,会严重阻塞(BLOCK)用户行为,该类型的卡顿通常会采用别的指标进行衡量。

深入浏览器内部性能优化,谷歌浏览器网站性能优化

1)Response响应时间:系统应当在100ms内对用户的输入作出响应,这种输入表示任何用户的交互行为,比如输入文本、点击、切换表单、开启动画等。在不阻塞用户交互行为的前提下可以对一些耗时昂贵的工作进行预运算,对于耗时 500ms以上的工作项应当通过信息反馈提前告知用户。

2)Animation动画:对于动画的交互,如touchmove、scroll等需要在16ms 内作出响应,而动画的帧率期望能够达到每秒60帧,即折算一帧16.6ms,而实际上渲染一帧的动画过程浏览器还存在着大量的样式计算、布局计算、线程调度、图层合成、光栅化等过程,因此纯JS的线程执行耗时应当控制在10ms 内。

3)Idle最大化空闲时间:空闲时间可以用来完成优先级不高的任务,通过50ms 为基准将延迟任务进行分片分组执行,目的时为了能够预留50ms的空闲时间给到主线程来对用户的输入进行即时响应,从而完成100ms内响应用户的标准。

4)Load:1s内完成站点加载。

上文中提到的很多名词概念,在此也做简单介绍。

  • 帧率(FPS):指的是一秒内合成帧的数量,常用FPS来描述,60FPS表示一秒内合成60帧。
  • 刷新率(Hz):指的是一秒内屏幕的刷新次数,安卓机通常为60Hz。
  • VSync(垂直同步):是一种定时中断技术,时间间隔为16.6ms,VSync信号保证了画面内的数据来源同步,磨平了屏幕刷新方式带来的上下屏数据来源的差异性。通常与双缓存、三缓存技术共同解决“画面撕裂”带来的卡顿。此处介绍一个可用于测试设备是否支持VSync的在线工具—设备测试VSync。
  • 双缓存机制:通过设计两种类型的缓存器Back Buffer、Frame Buffer分别为 CPU数据处理以及屏幕读取数据服务,解决了帧渲染过程中同时存在的读写逻辑冲突。
  • 三缓存机制:CPU与GPU在Back Buffer的使用权上存在竞争关系,导致GPU 占用时间段内CPU线程处于闲置状态,从而导致合成帧耗时较长,三缓存将 CPU与GPU对缓存器的使用状态也进行了隔离。

二、如何衡量卡顿

2.1 FPS与卡顿的关系

一般来说,我们通过页面的FPS来作为衡量页面卡顿的指标,但是用FPS来做卡顿的描述并不精确,举个例子:

1)电影*放播**的FPS=24,但是在电影*放播**过程并不会出现卡顿现象,说明了FPS低于30也不表示画面卡顿,从定义上来看FPS仅描述了一秒内的画面绘制次数,如果一秒内页面没有任何绘制的需求,FPS=0是很正常的。

2)对应的,FPS=60的页面如果在前200ms仅仅完成了首帧的渲染而剩余 800ms完成了剩余59帧的渲染,保证了一秒内的帧数,但是仍然会带来卡顿的现象。

2.2 新的衡量指标

SM(SMoothes)是Android端提出的相比于FPS更加准确的衡量卡顿现象的指标,SM更好的考虑了单位时长内的有效帧数占比,SM计算公式如下:

SM = FPS * (单位时长的总帧数 - 单位时长丢帧数) / 单位时长总帧数

三、浏览器动画渲染

为了能够更好的跟踪动画渲染过程的卡顿,我们应该更加清晰的去认知浏览器渲染的原理和过程,动画渲染的原理大图如下:

深入浏览器内部性能优化,谷歌浏览器网站性能优化

对上图进行链路总结如下:

  • CPU负责处理复杂的控制逻辑以及数据逻辑,在浏览器的渲染过程中生成 DOM树、CSSOM树、样式计算、布局计算、图层生成等,最后将矢量数据提交给合成器以及GPU。
  • GPU接受图层图块信息,并基于强大的硬件能力对图层进行分割切片、栅格化。GPU从结构上拥有更多的ALU单元,更擅长大量重复的计算以及矩阵变换,能够以流式并行的模式快速完成位图绘制,并将纹理数据存入缓存器。
  • 显示器通过VSync信号强制拉齐帧率与刷新率,当信号来临时访问前缓存器拿到最新的位图数据并用于上屏。

3.1 GPU扮演的角色

我们常说的硬件加速其实是通过GPU的特性来缓解CPU的计算压力,相比于 CPU,GPU拥有更多的ALU(算数逻辑单元),其特有的流式并行计算模式在矩阵变换的计算上有天然的优势,同时采用GPU的硬件加速可以对待处理的元素进行图层提升,实现了在画布更新过程中隔离非合成元素的目的,等到生成结束后通过相应位置的替换完成图层的合成,从而减少了CPU线程内回流和重绘的计算过程。

值得注意的是,浏览器本身为我们完成了很多优化的策略,比如在使用transform、opacity属性的时候浏览器会自动开启GPU加速,因此更推荐在动画过程中通过上述属性进行处理。除此之外,通过will-change定义的CSS属性值,浏览器也会预先将其关联的元素提升到新的图层,从而避免刷新屏幕时出现回流和重绘的过程。

3.2 合理避免回流和重绘

事实上,是否避免了回流和重绘的过程取决于对应的元素是否真正被提升到了合成层(Composite Layer)。即使使用了 transform、opacity 两个属性,如果浏览器不支持硬件加速导致图层没有被提升,那么在更新的过程中仍然会因此回流和重绘。

在此对常用于提升图层的方法进行罗列:

  • 3D transforms:translate3d,translateZ等;
  • video,canvas,iframe等元素;
  • 通过Element.animate()实现的opacity动画转换;
  • 通过СSS动画实现的opacity动画转换;
  • position: fixed;
  • will-change;
  • filter;
  • 有合成层(Composite Layer)后代同时本身overflow不为visible(如果本身是因为明确的定位因素产生的SelfPaintingLayer,则需要z-index不为auto)

四、浏览器工作流程

到此我们明确了在浏览器动画绘制过程中CPU以及GPU扮演的角色,为了更加清晰的明确一帧内CPU与GPU负责的具体工作,我们需要对浏览器内核的工作有一定的了解。

首先,浏览器是多进程工作的,进程结构如下:

深入浏览器内部性能优化,谷歌浏览器网站性能优化

我们常说的浏览器内核其实就是渲染进程(渲染引擎)。渲染引擎将从网络层获取请求的文档内容并解析渲染,整个渲染引擎的工作流程是渐进的,渲染引擎的工作流程如图所示:

深入浏览器内部性能优化,谷歌浏览器网站性能优化

对于其中的每一个部分展开解释有些复杂,本文不多做赘述。本文还是更加聚焦于画面渲染的Layout以及Painting过程,从这个角度来看,单帧内的渲染引擎主线程过程的Pipline如下图:

深入浏览器内部性能优化,谷歌浏览器网站性能优化

引入GPU加速计算后的主线程Pipline如下图:

深入浏览器内部性能优化,谷歌浏览器网站性能优化

对上述Pipline的各节点解读如下:

1)Input event Handlers代表浏览器的输入事件回调,该事件会被浏览器进程(Brower Process)捕捉,随后浏览器进程会将事件类型(如touchstart)及其坐标发送给渲染进程(Renderer Process),渲染进程通过查找事件目标并运行附加的事件*听器侦**来处理事件。

2)requestAnimationFrame允许定义一个回调函数,可提供给用户在当前帧布局计算以及绘制之前做一些预处理操作。

3)CPU在ParseHTML过程负责将浏览器不能识别的HTML文本转换为浏览器能识别的DOM对象。

深入浏览器内部性能优化,谷歌浏览器网站性能优化

4)CPU在RecalcStyles过程会解析CSS文件,依据CSS的样式继承以及层叠规则计算DOM节点的每一个元素的具体样式,并保存在ComputedStyle结构中。

深入浏览器内部性能优化,谷歌浏览器网站性能优化

5)CPU在Layout过程基于DOM Tree以及Compouted Style计算元素布局信息并生成Layout Tree(RenderObject),在该过程中不可见的DOM节点会被忽略,例如head标签下的全部内容以及display=none的DOM节点。

深入浏览器内部性能优化,谷歌浏览器网站性能优化

6)CPU在Update Layer Tree过程负责将相同z空间坐标的Layout Tree(RenderObject) 归并到相同的Paint Layer(RenderLayer),从而保证页面元素的合成顺序。通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层,但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。

深入浏览器内部性能优化,谷歌浏览器网站性能优化

7)CPU在Paint过程干了两件事:第一件事是完成绘制(Painting),即生成新生成/修改元素的绘制信息,包括图形信息,文本信息。第二件事是完成栅格化(Rasterization),对Painting的绘制信息进行消费,对于没有GPU加速的情况下,浏览器会通过CPU进行软件栅格化,软件渲染器没有使用GL将内容纹理块复制到后台缓冲区,而是使用 Skia(2D绘图库)的软件光栅化器来执行复制(并执行任何必要的矩阵计算和裁剪)。

8)CPU在Composite过程将图层信息提交(commit)到合成线程(Compositor Thread)中,在此线程中会对图层进行分块生成图块(图块是栅格操作的单位)同时借助GPU对额外的属性(如:will-change声明的元素,启用硬件加速的canvas)进行处理(图层变换、合成)。

9)GPU在Rasterize过程对图块纹理进行栅格化处理。

10)合成线程会在所有图层被Rasterize后打包发送到GPU进程,此时标志着一帧结束。

11)页面的所有信息在GPU内被处理后传入双缓存的Back Buffer中,下次垂直同步信号到达后互换前后缓存区位置,完成上屏。

到此,可以回答本章节开篇提出的问题,CPU给GPU传递的数据是经过布局、绘制计算的到的矢量图信息,而GPU只负责对矢量图进行像素化(光栅化)以及着色后将其存放到帧缓存区,等待下一个VSync信号的同步。

五、解决方案

回到移动端本身的问题,低端机面临的性能瓶颈问题往往由于其硬件能力不足导致的,如CPU性能(主频大小、Cache容量)、内核数、内存大小、DRAM大小、是够支持GPU加速以及GPU核数等。对于同样的页面逻辑,在低端机上执行,往往会带来更高的CPU占有率从而导致帧数丢失、更高的内存占有率从而带来更多的数据交换损失,而一些不支持GPU的机型中,单帧内的动画会依赖CPU进行软件渲染,这个过程会大量占用CPU主线程从而带来丢帧。

在了解低端机面临的瓶颈问题后,我们应该通过以下策略最大限度的降低性能损耗:

  • 开启适当的硬件加速(GPU加速),合理使用CSS属性进行动画绘制,如 transform、opacity、filter等。
  • 采用will-change对比较复杂的元素处理进行单独图层的提升(切忌对所有元素使用,过量的图层会加剧GPU合成时的功耗)。
  • 最小化动画的范围,可以有效减少帧数据(位图)的大小,降低GPU访问主存的时间损耗。
  • 使用脱离文档流的方式对元素进行动画,减少回流。
  • 避免通过父级元素对子元素进行访问,样式的访问链路过程会加剧样式树计算时的时间损耗。
  • 尽可能少的访问offsetWidth、top等元素样式属性,因为这些属性会引发回流和重绘,如果必须访问时,可以进行数据缓存,同时可以在rAF阶段进行访问,因为rAF后浏览器会进行布局的recalculate。
  • 对于没有GPU加速的机型,可采用降低动画的影响范围,降低纹理的尺寸的方式等加速主线程,从而提升页面响应速度。
  • 长任务切片,将连续串行的任务通过优先级设置和任务调度的方式切割,从而减少长任务占用主线程时无法及时对用户行为作出响应。