乐趣区

网页动画性能日志(一)

动画做多了,自然就要考虑性能,我打算出一个系列的日志,详细的讲解一下网页动画性能相关的知识,如果你已经可以运用 css3 canvas 来做动画,可以来参考一下。
<!–more–> 目前我做的最复杂的动画就是 360 搜索中 PC 端的天气动画。
<img src=”https://static.imwineki.cn/st…; width=”300″ height=”200″>
共包含 14 个动画场景,每个场景基本由 1 - 3 个独立的动画叠加而成,抽象动画共 12 个,从开发到优化完成共分为四期完成,一二期所有的动画均使用 canvas 完成,三期四期对动画性能进行大幅度改进,重构了部分代码。其中兼容低倍屏、高倍屏,canvas 绘制的折线图浏览器兼容到 IE6(具体实现参见:基于 canvas 折线图统计图),动画兼容到 IE9,低版本浏览器动画显示静态图片。所以以下我所有分析均依赖于该项目。
量化流畅的动画
首先我们先来简单的介绍一下动画原理,其实动画本身是不动的,它的实现原理是利用人眼的“视觉暂留”现象,在段时间内连续播放数副精致的画面,使肉眼银视觉残象产生错觉,而产生“动”的概念。
相关概念 <li> 帧(Frame):在动画过程中,每一幅静止画面即为一“帧”。
<li> 帧率(Frame per second):即每秒钟播放的静止画面的数量,单位是 fps(Frame per second)。
<li> 帧时长:每一幅静止画面的停留时间,单位一般是 ms(毫秒)。
<li> 跳帧 (掉帧 / 丢帧):在帧率固定的动画中,某一帧的时长远高于平均帧时长,导致其后续数帧被挤压而丢失的现象。
正常情况下浏览器渲染刷新频率稳定在 60fps 左右,人眼是可以看到流畅平滑的动画的,一般来讲低于 30fps 的动画,就会有卡顿。
工欲善其事必先利其器
来了解一下,怎样使用 chrome 这把利刃。
FPS Meter

使用这个工具你可以检测当前浏览器 GPU 渲染动画的帧率。

✔️ 高亮网页中需要重绘的区域
✔️ 将需要重回的区域用橘黄色的边框标注
✔️ 绘制帧率,帧速率分布,以及 GPU 缓存
✔️ 展示页面中减慢滚动的区域
✔️ 强制媒体类型来测试绘制和屏幕渲染

<img src=”https://static.imwineki.cn/st…; width=100 height=80>
当然以上只能在 chrome 中进行调试,如果你想在其他的浏览器 FPS 的检测,你可以使用 stats.js,侦听全局或指定位置的帧率,JS 实现,所有浏览器可用。

Timeline

具体使用方式可以参考:chrome 官方文档
profiles

taskmanager

浏览器内核
各厂出品的浏览器所用的渲染引擎不尽相同:IE 使用 Trident,FireFox 使用 Gecko,Safari 使用 WebKit,Chrome 28+ 和 Opera 15+ 使用的是 Blink(WebKit 的分支)现代的浏览器通常会有两个重要的执行线程,这 2 个线程协同工作来渲染一个网页:<li> 主线程 <li> 合成线程
一般情况下,主线程负责:<li> 运行 js<li> 计算 HTML 元素的 css 样式 <li> 页面布局 layout<li> 将元素绘制到一个或多个位图中 <li> 将这些位图交给合成线程
合成线程负责:<li> 通过 GPU 将位图渲染到屏幕 <li> 通知主线程更新页面中可见或即将变成可见的部分的位图 <li> 计算页面中可见部分 <li> 计算出当你在滚动页面时哪部分是即将变成可见的 <li> 当你滚动页面时将相应位置的元素移动到可视区域
长时间执行 JavaScript 或渲染一个很大的元素会阻塞主线程,在这期间,它将无法响应用户的交互。
相反,合成线程则会尽量去响应用户的交互。当一个页面发生变化时,由于当今大多数设备的屏幕刷新率都是 60 次 / 秒,所以合成线程也会以每秒 60 帧的间隔去不断重绘这个页面,即使这个页面不完整。
当用户滚动页面时,合成线程会通知主线程更新页面中最新可见部分的位图。但是,如果主线程响应地不够快,合成线程不会保持等待,而是马上绘制已经生成的位图,还没准备好的部分用白色进行填充。
也就是说 js 是单线程的,但浏览器是多线程的,感兴趣的小伙伴可以看一下我之前翻译的谷歌日志浏览器多进程架构。
浏览器的渲染机制
大多数设备的屏幕刷新率都是 60 次 / 秒,浏览器对每一帧画面的渲染工作需要在 16 毫秒(1 秒 / 60 = 16.66 毫秒)之内完成。但实际上,在渲染某一帧画面的同时,浏览器还有一些额外的工作要做(比如渲染队列的管理,渲染线程与其他线程之间的切换等等)。因此单纯的渲染工作,一般需要控制在 10 毫秒之内完成,才能达到流畅的视觉效果。如果超过了这个时间限度,页面的渲染就会出现卡顿效果,也就是常说的 jank,它是很糟糕的用户体验。
在 web 页面中,代码就是经过大概以下步骤转换成屏幕上的显示像素:

1.JavaScript:一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。
2. 计算样式:这个过程是根据 CSS 选择器,比如.headline 或.nav > .nav_item,对每个 DOM 元素匹配对应的 CSS 样式。这一步结束之后,就确定了每个 DOM 元素上该应用什么 CSS 样式规则。
3. 布局:上一步确定了每个 DOM 元素的样式规则,这一步就是具体计算每个 DOM 元素最终在屏幕上显示的大小和位置。web 页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如,<body> 元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。
4. 绘制:本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个 DOM 元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
5. 渲染层合:由上一步可知,对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。
上述过程的每一步中都有发生 jank 的可能,因此一定要弄清楚你的代码将会运行在哪一步。
虽然在理论上,页面的每一帧都是经过上述的流水线处理之后渲染出来的,但并不意味着页面每一帧的渲染都需要经过上述五个步骤的处理。实际上,对视觉变化效果的一个帧的渲染,有这么三种 常用的 流水线:
1.JS / CSS > 计算样式 > 布局 > 绘制 > 渲染层合并

如果你修改一个 DOM 元素的”layout”属性,也就是改变了元素的样式(比如宽度、高度或者位置等),那么浏览器会检查哪些元素需要重新布局,然后对页面激发一个 reflow 过程完成重新布局。被 reflow 的元素,接下来也会激发绘制过程,最后激发渲染层合并过程,生成最后的画面。
2.JS / CSS > 计算样式 > 绘制 > 渲染层合并

如果你修改一个 DOM 元素的“paint only”属性,比如背景图片、文字颜色或阴影等,这些属性不会影响页面的布局,因此浏览器会在完成样式计算之后,跳过布局过程,只做绘制和渲染层合并过程。
3.JS / CSS > 计算样式 > 渲染层合并

如果你修改一个非样式且非绘制的 CSS 属性,那么浏览器会在完成样式计算之后,跳过布局和绘制的过程,直接做渲染层合并。第三种的性能最为理想,一般来说对于动画和滚动这种复合很重的渲染,我们就尽量向第三种靠拢。
性能优化是一门减法艺术,我们要本着经历简化页面徐然过程,然后使每一步的渲染尽可能高效。
浏览器中 CPU vs GPU,如何抉择?
我们先来说说 GPU,大多数手机、平板电脑、和计算机都配备了 GPU 芯片,GPU 有着非常专业的定位,这意味着 GPU 非常擅长做某些事情(比如绘图),但在其他方面则没什么优势。
现在,我们来看看 CPU 和 GPU 的内部特点 CPU(Central Processing Unit),GPU(Graphics Processing Unit)翻译过来,第一个叫做中央处理器,后者叫做视觉处理器,换言之,同样是计算机中用于计算的核心组件,CPU 主要负责通用计算,GPU 主要负责专用计算。我们来看看下面的这个图理解一下:

翻译一下:<li>_强大的 ALU – 降低操作延时_<li>_巨大的缓存器 - 将长延迟内存访问转换为短延迟缓存访问_<li>_复杂的控制器 - 分支预测用于减少分支延迟   - 数据转发,减少数据延迟_

翻译一下:<li>_小缓存 - 用来提高内存吞吐量_<li>_小控制器 - 没有分支预测,没有数据转发_<li>_高效能 ALU – 很多长延时,但是有大量的吞吐流量_
可以看到,CPU 中包含 Control(控制层),Cache(缓存层),ALU(算术逻辑单元),其中 ALU 是主要负责进行简单运算的。而 GPU 中则可以明显的看到包含大量的 ALU 模块和少量的 Cache 和 Control 模块。
算术逻辑单元(英语:Arithmetic Logic Unit, ALU)[1] 是中央处理器的执行单元,是所有中央处理器的核心组成部分,由及闸和或闸构成的算数逻辑单元,主要功能是进行二进制的算术运算,如加减乘 (不包括整数除法)。基本上,在所有现代 CPU 体系结构中,二进制都以二补数的形式来表示。—– 维基百科
所以可以清晰地从结构中看出,CPU 更擅长于逻辑控制,串行运算,GPU 更擅长于大规模并发计算。举一个简单的例子,一个教授,带着 20 个学生完成一个项目,现在项目需要处理 1000 次 100 以内的加减乘除运算,这项工作其实并不需要任何逻辑处理,只是大量的工作量的堆叠,这时,就不需要教授亲力亲为,教授可以将任务分配给这 20 个学生分工完成这项工作,其中教授的角色就相当于 CPU,20 个学生总体相当于 GPU,每个学生就等于一个 ALU。
GPU 的处理图像的优势:<li> 绘制位图到屏幕上 <li> 一遍又一遍地绘制相同的位图 <li> 将同一位图绘制到不同位置,执行旋转以及缩放处理 <li> 具有多核简单计算能力,可以处理大量计算数据
GPU 的慢在于:<li> 将位图加载到它的显存中
JS 动画
缺点:JavaScript 在浏览器的主线程中运行,而其中还有很多其他需要运行的 JavaScript、样式计算、布局、绘制等对其干扰。这也就导致了线程可能出现阻塞,从而造成丢帧的情况。
优点:JavaScript 的动画与 CSS 预先定义好的动画不同,可以在其动画过程中对其进行控制:开始、暂停、回放、中止、取消都是可以做到的。而且一些动画效果,比如视差滚动效果,只有 JavaScript 能够完成。
CSS 动画
缺点:缺乏强大的控制能力。而且很难以有意义的方式结合到一起,使得动画变得复杂且易于出问题。
优点:浏览器可以对动画进行优化。它必要时可以创建图层,然后在主线程之外运行,也就是开启 GPU 加速。
一般来说,Chrome 中满足以下任意情况就会创建图层:<li> 3D 或透视变换(perspective transform)CSS 属性 <li> 使用加速视频解码的 <video> 节点 <li> 拥有 3D(WebGL)上下文或加速的 2D 上下文的 <canvas> 节点 <li> 混合插件(如 Flash)<li> 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素 <li> 拥有加速 CSS 过滤器的元素 <li> 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)<li> 元素有一个 z -index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
需要注意的是,如果图层中某个元素需要重绘,那么整个图层都需要重绘。比如一个图层包含很多节点,其中有个 gif 图,gif 图的每一帧,都会重回整个图层的其他节点,然后生成最终的图层位图。所以这需要通过特殊的方式来强制 gif 图属于自己一个图层(translateZ(0) 或者 translate3d(0,0,0)),CSS3 的动画也是一样。
展望
通过 web workers 这样的多线程来实现动画

好啦!基本概念已经铺垫完了,感兴趣的话,就来看看在业务线中如何小试牛刀。网页动画性能日志(二)
转载请说明出处!
参考链接:<li>http://blog.csdn.net/leer168/article/details/25917093
<li>https://segmentfault.com/a/1190000000490328

退出移动版