优化动画卡顿:卡顿缘由分析及优化方案

目录

1、动画卡顿分析

动画卡顿的缘由

大多数设备的刷新频率是60次/秒,也就是1秒钟的动画是由60个画面连在一块儿生成的,因此要求浏览器对每一帧画面的渲染工做要在16ms内完成。当渲染时间超出16ms时,1秒钟内少于60个画面生成,就会有不连贯、卡顿的感受,影响用户体验。css

页面渲染流程

一个页面帧在客户端的渲染分为如下几步:
web

页面渲染流程 来源:Google

  1. JavaScript:JavaScript实现动画效果,DOM操做等。
  2. Style(样式计算):确认每一个DOM元素应用的CSS样式规则。
  3. Layout(布局):计算每一个DOM元素最终在屏幕上的大小和位置。因为DOM元素的布局是相对的,因此当某个元素发生变化影响了布局时,其余元素也会随之变化,则须要回退从新渲染,这个过程称之为reflow。
  4. Paint(绘制):在多个层上绘制DOM元素的文字、颜色、图像、边框和阴影等。
  5. Composite(Render Layer合并):按照合理的顺序合并图层并显示到屏幕上。 浏览器在实际渲染页面的时候须要通过一系列的映射,由HTML页面构建出来的DOM树到最终的图层,映射过程以下图(来源:参考[3])所示(注意下图类名在后续有所更改,RenderObject->LayoutObject,RenderLayer->PaintLayer):
    The Compositing Tree
  • Node->RenderObject:DOM树的每一个Node都有一个对应的RenderObject(一对一关系,RenderObject包含了Node的内容);ajax

  • RenderObject -> RenderLayer:一个或多个RenderObject对应一个RenderLayer(多对一),RenderLayer用于保证元素之间的层级关系,通常来讲位于同一位置的且层级相同的元素位于同一个Render Layer,只有某些特殊的RenderObject会专门建立一个新的渲染层,其余的RenderObject与第一个拥有RenderLayer的祖先元素共用一个。常见的生成RenderLayer的RenderObject拥有如下的一种特征参考[3]chrome

    • 页面根元素
    • 有CSS定位属性(relative, absolute, fixed, sticky)
    • transparent不为1
    • overflow不为visible
    • 有CSS mask属性
    • 有CSS box-reflect属性
    • 有CSS filter属性
    • 3D或硬件加速的2D canvas元素
    • video元素
  • RenderLayer -> GraphicsLayer:一个或多个RenderLayer对应一个GraphicsLayer(多对一),某些被认为是Compositing Layer的RenderLayer单独对应一个GraphicsLayer,其余RenderLayer与第一个拥有GraphicsLayer的祖先元素共用一个GraphicsLayer。每一个GraphicsLayer有一个GraphicsContext用于绘制其对应的RenderLayers,合成器将GraphicsContexts的位图合成,最终显示到屏幕上。渲染层提高为合成层的缘由以下:canvas

    • 有3D transform属性
    • 有perspective属性
    • 3D canvas或硬件加速的2D canvas
    • 硬件加速的iframe元素(如iframe嵌入的页面有合成层,合成层须要硬件加速)
    • 使用了硬件加速的插件,如flash
    • 对opacity/transform属性应用了animation/transition(当animation/transition为active)
    • 子元素是compositing layer
    • 兄弟元素是compositing layer,与当前的非composting layer有重叠,层级低于当前层
    • 有will-change属性

2、优化方法

在网上能够看到不少的优化方案总结,大佬们都写的很好。windows

Talk is cheap. Show me the code.浏览器

结合页面渲染流程,这里将结合一些测试代码,分析动画的各类优化方案和效果:性能优化

  • JavaScript:优化JavaScript的执行效率
    • requestAnimationFrame代替setTimeoutsetInterval
    • 可并行的DOM元素更新划分为多个小任务
    • DOM无关的耗时操做放到Web Workers
  • Style:下降样式计算复杂度和范围
    • 下降样式选择器的复杂度
    • 减小须要执行样式计算的元素个数
  • Layout:避免大规模、复杂的布局
    • 避免频繁改变布局
    • 用flexbox布局替代老的布局模型
    • 避免强制同步布局事件
  • Paint/Composite:GPU加速
    • 将移动或渐变元素由渲染层(RenderLayer)提高为合成层(Compositing Layer)
    • 避免提高合成层的陷阱

JavaScript:优化JavaScript的执行效率

1. requestAnimationFrame代替setTimeoutsetInterval

为何setTimeoutsetInterval很差?
因为js是单线程执行,因此为了防止某个任务执行时间过长而致使进程阻塞,js中存在异步队列的概念,对于如setTimeoutajax请求都是把进程放到了异步队列中,当主进程为空时才执行异步队列中的任务。因此 setTimeoutsetInterval没法保证回调函数的执行时机,可能会在一帧以内执行屡次致使屡次页面渲染,浪费CPU资源甚至产生卡顿,或者是在一帧即将结束时执行致使从新渲染,出现掉帧的状况。
requestAnimationFrame是怎么优化的?服务器

  • CPU节能,当页面被隐藏或最小化时,暂停渲染。
  • 函数节流,其循环间隔是由屏幕刷新频率决定的,保证回调函数在屏幕的每一次刷新间隔中只执行一次。

优化效果具体如何?DEMO
经过chrome的performance面板查看具体表现的差异。
经过setTimeout进行了3次渲染,并且有长时间帧出现:
网络

setTimeout

使用 requestAnimationFrameDOM操做部分合并,只进行了2次渲染,长时间帧也被优化:
requestAnimationFrame

2. DOM无关的耗时操做放到Web Worker

Web Worker的好处是什么?
JavaScript是单线程的,若是频繁的进行耗时操做(如实时更新数据),就会形成拥堵,影响用户交互体验。Web Worker的做用在于为JavaScript建立了多线程环境,worker线程在后台运行,受主线程控制,二者互不干扰。worker线程负担高延迟且UI无关的任务,主线程负责UI交互就会相对流畅。
须要注意

  • Web Worker没法操做DOM,本质上只是将数据刷新和页面渲染拆开执行。
  • Web Worker遵循同源策略且限制本地访问。
  • 用一次多余的网络请求和浏览器线程资源来换取高效执行。

优化效果具体如何?DEMO
能够经过chrome的performance面板查看具体表现的差异: 不使用web worker,减小了一次网络请求,可是出现了长时间帧,有卡帧的风险。

不使用worker

使用了 web worker以后,耗时操做无关的任务再也不被阻塞,可是增长了网络延迟。若是在项目中使用worker,初始化时间须要好好斟酌。
使用worker

可考虑的应用场景

  • 轮询服务器获取数据
  • 频繁的数据上报
  • 耗时的数据处理

Style:下降样式计算复杂度和范围

1. 下降样式选择器的复杂度?

下降样式选择器的复杂度是经常被提出的一个优化方法,实际上这个方法的效果比较微弱,根据Ivan Curic的文章[5]的测试方法(DEMO),在一个拥有50000个节点的页面中,不一样选择器复杂度对于性能的影响不会超过20ms,而通常状况下,页面的节点数都不会达到这个数量。
优化效果微弱的缘由在于浏览器引擎对选择器速度进行了优化,不一样引擎的性能优化方案不一样,因此开发者的优化是否有效是难以预测的,至少对于静态元素的优化性价比是极低的。
经过测试能够确认的一点是,应当减小伪类选择器和过长的选择器的使用。推荐按照如OOCSS、BEM等命名规范来组织CSS,优势是在微弱优化性能的同时也提升了代码可维护性。

2. 减小须要执行样式计算的元素个数

这一点是针对较早的浏览器而言,较早的浏览器如改变了body元素上的一个类,则其子元素都须要从新计算样式。
现代浏览器都进行了优化,因此优化效果要视具体应用场景而言。目前还没有挖掘到应用例子,后期若有发现回来填坑。

Layout:避免大规模、复杂的布局

1. 避免频繁触发布局

不一样的属性致使的渲染成本不尽相同,这一点在css动画时对比尤为明显。触发layout或者paint的动画属性尤为消耗性能,因此应当尽可能使用transformopacity做为动画属性,若是没法实现则考虑采用JavaScript实现动画。
性能差异有多大? 以width和transform为例,分别实现动画的性能差异:DEMO
经过width实现动画,帧率较低且曲线抖动明显,右下角也给出了一帧的渲染过程,触发了样式计算,布局,绘制和渲染层合并:

width实现动画

经过transform实现动画,能够发现帧率虽然也低可是平稳,渲染过程只触发了样式计算和、绘制和渲染层合并(仅当元素为合成层时,不会触发绘制。后面将详细讲述):
transform实现动画

2. 用flexbox布局替代老的布局模型

经常使用的经典布局方案有基于浮动的布局、基于绝对定位的布局,flexbox布局相较而言更加高效。在能用flexbox布局的项目中,尽可能用flexbox布局。如下DEMO尝试用三种布局方式渲染同样的界面效果来测试性能:
绝对布局:对于每个元素都须要惟一的定位坐标,当元素较多时,CSS文件偏大,致使在样式计算上花费了较多的时间。

绝对布局

浮动布局:浮动元素之间定位会互相影响,部分浮动元素也受到文档流影响,致使布局所需时间较长。
浮动布局

弹性布局:对比前两种布局方案而言,性能有较显著的提高。
弹性布局

3. 避免强制同步布局事件

什么是强制同步布局?
前面提到了页面渲染流程是JavaScript->Style->Layout->Paint->Composite,强制同步布局就是强制浏览器在执行JavaScript脚本前先执行布局。
什么状况会致使强制同步布局?
JavaScript运行时,获取到的元素属性样式都是上一帧的数值,因此若是在当前帧的渲染流程中,获取当前帧的某个元素属性以前对该元素进行了修改,浏览器就必须先应用属性再执行JavaScript逻辑,简而言之就是DOM先写后读操做,尤为是连续的读写操做,对浏览器的性能影响更大。 对性能影响有多大?DEMO
DEMO经过改变1000个节点的属性,测试强制同步布局事件对性能的影响,具体参照下图。能够发现性能的损耗是极大的,连续的读写操做致使连续的强制同步事件触发,JavaScript执行时间变得很长:

强制同步布局

Paint/Composite:GPU加速

1. 将移动或渐变元素由渲染层(RenderLayer)提高为合成层(Compositing Layer)

注:可在Chrome的开发者工具的layers面板查看合成层,layers面板打开方法command+shift+p(mac)/ctrl+shift+p(windows) -> show layers 将复杂/频繁变化的元素提高到合成层,这样的好处是该元素绘制的时候不会触发其余元素的绘制。渲染层提高为合成层的缘由以下(注意如下缘由是在渲染层的基础之上):

  • 有3D transform属性
  • 有perspective属性
  • 3D canvas或硬件加速的2D canvas
  • 硬件加速的iframe元素(如iframe嵌入的页面有合成层,合成层须要硬件加速)
  • 使用了硬件加速的插件,如flash/iframe
  • 对opacity/transform属性应用了animation/transition(当animation/transition为active)
  • will-change属性为opacity、transform、top、left、bottom、right
  • 子元素是compositing layer
  • 兄弟元素是compositing layer,与当前的非composting layer有重叠,composting layer的层级低于非composting layer层

为何会有性能提高?

  • 只重绘须要重绘的部分
  • GPU加速:合成层的位图直接由GPU合成,比CPU处理速度更快

性能提高有多少? DEMO 经过demo能够看到,提高为合成层以后,paint所需的时间大大减小。

render layer -> compositing layer

提高合成层是否是越多越好?
能够看到提高合成层后,paint时间大大降低。可是合成层的建立须要消耗额外的内存和管理资源,过多的合成层给页面带来的内存开销很大,DEMO建立了5000个元素,所有元素都提高为合成层与不提高时的内存消耗进行对比。这一点在移动端尤为须要注意,相比较于PC,移动设备的内存资源更加紧张。

过多合成层

只提高动画元素的渲染层
基于提高为合成层来提高性能的原理,当页面其余部分绘制比较复杂且相对静态时,咱们能够考虑将动画元素单独提高为合成层,减小动画元素对页面其余元素的影响。

2. 避免提高合成层的陷阱

回顾一下提高为合成层的最后一个缘由:兄弟元素是compositing layer,与当前的非composting layer有重叠,composting layer的层级低于非composting layer层。
这种状况下致使的提高合成层通常都是预期外的。其缘由与屏幕的渲染流程有关,咱们回忆一下页面映射的最后一步,每个Compositing Layer对应一张位图,合成器最后将这些位图根据层级关系合并起来最终输出到屏幕。此时咱们假设A是已知的合成层,而B理想中应当是普通渲染层,其层级关系如图所示:

层级陷阱

B做为普通渲染层与父级元素位于同一张位图,A单独在一张位图,此时合并的时候层级就会出现问题,若是直接将B置于A之上,有可能致使层级低于A的B的父元素反而显示在了A之上,反之A,B的层级关系就不对了。浏览器此时的解决方案,就是将B也单独出来做为compositing layer进行渲染,致使了意料外的compositing layer生成。 这种时候第一直觉就是避免重叠的发生不就行了嘛?然而事情并不简单。在查找资料的时候发现了一个神奇宝贝—— assumedOverlap。字面意思是假设重叠,对于没法/难以判断是否会与compositing layer重合的某些元素,浏览器假设会发生重叠,提高为compositing layer。
对此浏览器也进行了优化的,经过层压缩(Layer Squashing)处理,将与合成层有重叠且连续多个的渲染层合并为一个合成层。防止因为重叠致使的提高合成层过多,致使的层爆炸(Layer Explosion),可参考 DEMO
然而层压缩仍是有解决不了的状况,查看 源码能够列出如下缘由(注意一下都是在重叠/假设重叠的前提下):

  • scrollsWithRespectToSquashingLayer:渲染层相对于压缩层滚动,当滚动的渲染层与合成层重叠时,会有新的合成层生成且没法压缩。DEMO(这个例子不是很好,codepen用iframe嵌入,整个iframe都变成了合成层,若是想看效果能够在本地看)
  • squashingSparsityExceeded:渲染层压缩后会致使压缩层过于稀疏。DEMO
  • squashingClippingContainerMismatch:渲染层和压缩层的裁剪容器(clip container)不一样,简单理解就是重叠的渲染层的容器overflow类型不一样。DEMO
  • squashingOpacityAncestorMismatch:渲染层与压缩层的继承自祖先的opacity属性不一样。DEMO
  • squashingTransformAncestorMismatch:渲染层与压缩层的继承自祖先的transform不一样。DEMO
  • squashingFilterAncestorMismatch:渲染层与压缩层的继承自祖先的filter属性不一样,或者是渲染层自己有filter属性。DEMO
  • squashingWouldBreakPaintOrder:没法在不打乱渲染顺序的前提下压缩(e.g. 父元素有mask/filter属性,子元素与压缩层overlap,则假如合并了,父元素的mask/filter属性没法局部应用在压缩层,致使渲染结果有误)。DEMO
  • squashingVideoIsDisallowed:video元素没法被压缩。DEMO
  • squashedLayerClipsCompositingDescendants:当合成层是被剪切的子元素时,与之重叠的渲染层没法被压缩。DEMO
  • squashingLayoutPartIsDisallowed:没法压缩frame/iframe/plugin。
  • squashingReflectionDisallowed:没法压缩有reflection属性的渲染层。 DEMO
  • squashingBlendingDisallowed:没法压缩有blend mode属性的渲染层。DEMO
  • squashingNearestFixedPositionMismatch:渲染层的最近fixed元素与压缩层不一样,没法被压缩。DEMO

当发现页面明明没有什么内容却比较卡的时候能够检查一下是否是这个缘由,如下给出常见的层压缩解决不了的状况:

  1. transform动画的元素,其后的元素为relative/absolute定位
    缘由:relative元素和relative下的absolute元素因为assumedOverlap缘由都被被提高为合成层,又因为设置了overflow:hidden,基于前面提到的squashingClippingContainerMismatch,渲染层与合成层的裁剪容器不一样,致使没法层压缩,出现过多的合成层。 解决方法:为动画的元素设置z-index扰乱compositing layer的排序。DEMO

3、参考

本文结构主要参照文章[1],对其中的一些优化点进行了实际测试和扩展,也算是一篇读后感吧~
关于层压缩部分状况过于复杂,没找到什么资料,感受尚未彻底吃透,后面有机会再从新整理一下。感恩如下大佬!

  1. 深度剖析浏览器渲染性能原理,你到底知道多少? www.jianshu.com/p/a32b890c2…
  2. Optimizing CSS: ID Selectors and Other Myths www.sitepoint.com/optimizing-…
  3. GPU Accelerated Compositing in Chrome www.chromium.org/developers/…
  4. GPU加速是什么 aotu.io/notes/2017/…
  5. Blink Compositing Update: Recap and Squashing docs.google.com/presentatio…
  6. 无线性能优化:Composite taobaofed.org/blog/2016/0…

撒花完结~欢迎指教~

相关文章
相关标签/搜索