WARNING

在无阻塞的情况下setInterval(() => {left1 += 10;div1.style.left = ${left1}px}, 0)

window.requestAnimationFrame(step);

setInterval(当setTimeout的定时的时间小于4ms,一律按照4ms来算)页面展示上快于requestAnimationFrame

主线程执行left的值大于requestAmimationFrame

在前端实现动画有三种主流的方式

  • Canvas
  • CSS3
  • Dom 当然,DOM+JS的这种方式由于极易引起浏览器重绘或回流,有非常大的性能风险,对于这种动画的优化方法就是不用DOM进行动画操作。

# CSS3动画优化原理

要想进行CSS的动画必须了解一定的浏览器原理,我们会介绍浏览器原理的几个概念,图层、重绘、回流

# 图层

浏览器在渲染一个页面时,会将页面分为很多个图层,图层有大有小,每个图层上有一个或多个节点。在渲染DOM的时候,浏览器所做的工作实际上是:

  • 获取DOM后分割为多个图层
  • 对每个图层的节点计算样式结果(Recalculate style -- 样式重计算)
  • 为每个节点生成图形和位置(Layout--回流和重布局)
  • 将每个节点绘制到图层位图中(Paint Setup和Paint--重绘)
  • 图层作为纹理上传值GPU
  • 符合多个图层到页面上生成最终屏幕图像(Composite Layers--图层重组)

# 回流

有些节点,当你改变它时,会需要重新布局(这也就意味着需要重新计算其他被影响的节点的位置和大小)

这种情况下,被影响的DOM树越大(可见节点),重绘所需要的时间就会越长,而渲染一帧动画的时间也响应变长。所以需要尽力避免这些属性

一些常用的改变时会触发布局的属性

盒子模型相关属性会触发重布局

  • width
  • height
  • padding
  • margin
  • display
  • border-width
  • border
  • min-height

定位属性和浮动也会触发重布局

  • top
  • bottom
  • left
  • right
  • position
  • float
  • clear

改变节点内部文字结构也会触发重布局

  • text-align
  • overflow-y
  • font-weight
  • overflow
  • font-family
  • line-height
  • vertival-align
  • white-space
  • font-size

# 重绘

修改时候只触发重绘的属性有:

  • color
  • border-style
  • border-radius
  • visibility
  • text-decoration
  • background
  • background-images
  • background-position
  • background-repeat
  • background-size
  • outline-color
  • outline
  • outline-style
  • outline- width
  • box-shadow 这些属性都不会修改节点的大小和位置,自然不会触发重布局,但是节点内部的渲染效果进行了改变,所以只需要重绘就可以了.

# CSS3动画优化

经过上面的介绍,我们大致了解了浏览器的绘制原理,那么想进行css动画优化需要遵循一下原则

  1. 尽量将动画放在一个独立图层,这样可以避免动画效果影响其他渲染层的元素
  2. 尽量避免回流和重绘
  3. 尽量使用GPU,速度更快

因此我们需要创建独立的合成层

那么如何才能创建合成层呢?

直接原因

  • 硬件加速的iframe元素(比如iframe切入的页面中有合成层)demo
  • video元素
  • 覆盖在video元素上的视频控制栏
  • 3D或者硬件加速的2D canvas元素
    • demo:普通2D Canvas不会提升为合成层
    • demo:3d Canvas提升为合成层
  • 硬件加速的插件,比如flash等等
  • 在DPI较高的屏幕上,fix定位的元素会自动的被提升到合成层中。但在DPI较低的设备上却并非如此,因为这个渲染层的提升会使得字体渲染方式由字像素变为灰阶
  • 有3D transform
  • backface-visibility为hidden
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)demo
  • 后代元素原因:
    • 有合成层后代同时本身有 transform、opactiy(小于 1)、mask、fliter、reflection 属性 demo
    • 有合成层后代同时本身 overflow 不为 visible(如果本身是因为明确的定位因素产生的 SelfPaintingLayer,则需要 z-index 不为 auto) demo
    • 有合成层后代同时本身 fixed 定位 demo
    • 有 3D transfrom 的合成层后代同时本身有 preserves-3d 属性 demo
    • 有 3D transfrom 的合成层后代同时本身有 perspective 属性 demo

提升合成层的最好方式是使用CSS的will-change属性。从上一节合成层产生的原因中,可以知道will-change设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。

关于合成层的更多知识可以移步淘宝FED的无线性能优化:Composite

# 如何避免重绘和回流

具体而言,就是多使用transform 或者 opacity 来实现动画效果,上述方法在合成层使用不会引起重绘和回流.

# 如何利用GPU加速

  • opacity
  • translate
  • rotate
  • scale

# Canvas优化

CSS虽然更加简单也更加保证性能的下限,但是要实现更加复杂可控的动画,那就必须用到Canvas + Javascript这个组合了

Canvas作为浏览器提供的2D图形绘制API本身有一定的复杂度,优化的方法非常多,我们仅仅介绍几种比较主流的优化方式.

# 运行requestAnimationFrame

很多时候我们会使用setInterval这种定时器来完成js动画循环,但是定时器在单线程的js环境下并不可靠,并不能保证严格按照开发者的设置来进行动画循环,因此很多时候setInterval会引起掉帧的情况。

因此requestAnimationFrame的优势就体现出来了

  • 性能更好:优点是它能够将所有的动画都放在一个浏览器重绘周期里去做,这样能保存你的CPU的循环次数,提高性能
  • 开销更小: requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省GPU开销

# 离屏Canvas

离屏渲染的原理就是把离屏Canvas当成一个缓存区。把需要重复绘制的画面数据进行缓存起来,较少调用canvas的API的消耗

  1. 创建离屏canvas;
  2. 设置离屏canvas的宽高; 3, 在离屏canvas中进行绘制;
  3. 在离屏canvas的全部或部分绘制到正在显示的canvas上

# 避免浮点运算

利用 canvas进行动画绘制时,如果计算出来的坐标是浮点数,那么可能会出现 CSS Sub-pixel的问题,也就是会自动将浮点数值四舍五入转为整数,那么在动画的过程中,由于元素实际运动的轨迹并不是严格按照计算公式得到,那么就可能出现抖动的情况,同时也可能让元素的边缘出现抗锯齿失真 这也是可能影响性能的一方面,因为一直在做不必要的取证运算.

# 较少调用Canvas API

canvas也是通过操纵 js来绘制的,但是相比于正常的 js操作,调用 canvas API将更加消耗资源,所以在绘制之前请做好规划,通过 适量 js原生计算减少 canvas API的调用是一件比较划算的事情.

比如,作粒子效果时,尽量少使用圆,最好使用方形,因为粒子太小,所以方形看上去也跟圆差不多。至于原因,很容易理解,我们画一个圆需要三个步骤:先beginPath,然后用arc画弧,再用fill进行填充才能产生一个圆。但是画方形,只需要一个fillRect就可以了。虽然只是差了两个调用,当粒子对象数量达到一定时,这性能差距就会显示出来了。

# web worker

在进行某些耗时操作,例如计算大量数据,一帧中包含了太多的绘制状态,大规模的 DOM操作等,可能会导致页面卡顿,影响用户体验.

web worker最常用的场景就是大量的频繁计算,减轻主线程压力,如果遇到大规模的计算,可以通过此 API分担主线程压力,此 API兼容性已经很不错了,既然 canvas可以用,那 web worker也就完全可以考虑使用.

# 资料

前端动画性能优化 (opens new window)

页面代码合并_requestAnimationFrame详解以及无线页面优化 (opens new window)

requestAnimationFrame (opens new window)

前端浏览器刷新渲染机制是什么? (opens new window)

# animation(Css)

# 兼容性与属性

猛戳这里 (opens new window)查看兼容性

  • animation-name: 动画名称
  • animation-duration: 动画时长
  • animation-timing-function: 动画执行方式
  • animation-delay: 动画延迟时间
  • animation-iteration-count: 动画执行次数
  • animation-direction: 是否反向执行动画
  • animation-fill-node: 动画执行前后的样式
.box {
    width: 200px;
    heigh: 200px;
    background-color: aqua;
    position:absolute;
    left: 0;
    top: 0;
    animation: test 3s linear 2s infinite;
}

@keyframes test {
    from {}
    to {
        width: 50px;
        height: 50px;
        background-color: red;
        opacity: 0.5;
        left: 500px;
        top: 500px;
    }
}

<div class="box"></div>

# requestAnimationFrame(JS)

# 兼容性与基本概念

  • 优势:

    • 浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的效果
    • 一旦页面不处于浏览器的当前标签,就会停止刷新,这就节省了CPU、GPU和电力
  • 使用

    • 持续调用requestAnimFrame即可
    • 可以使用cancelAnimationFrame清除动画

# 举例

#anim  {
    position: absolute;
    left: 0;
    width: 150px;
    height: 150px;
    line-height: 150px;
    background: aqua;
    color: white;
    border-radius: 10px;
    padding: 1em;
}

<div id="anim">Click here to start animation</div>

// 兼容处理
window.requestAnimFrame = (function() {
    return (
        window.requestAnimationFrame || 
        window.wikitRequestAnimationFrame || 
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function (callback, element) {
            window.setTimeout(callback, 1000/ 60);
        }
    )
})()

var elem = document.getElementById('anim');
var startTime = undefined;

function render(time) {
    time = Date.now();
    if (startTime === undefined) {
        startTime = time;
    }
    elem.style.left = ((time - startTime) / 10) % 300 + 'px';
    elem.style.top = ((time - startTime) / 10) % 300 + 'px';
    elem.style.borderRadius = ((time - startTime) / 10) % 300 + 'px';
    elem.style.opacity = Math.floor((time - startTime / 100)) % 2 === 0 ? 1 : 0.3
}

elem.onclick = function() {
  (function animloop() {
    render()
    requestAnimFrame(animloop)
  })()
}

参考文档 (opens new window)

# window.requestAnimationFrame

参考地址 (opens new window)

window.requestAnimationFrame()告诉浏览器--你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

WARNING

注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame();

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入该方法的动画函数(即你的回调函数)。 回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数想匹配。为了提高性能和电池寿命,因此在大多数浏览器中,当requestAnimationFrame()运行在后台标签页或者隐藏的iframe里时,requestAnimationFrame()会被暂时停用以提升性能和电池寿命。

回调函数会被传入DOMHighResTimeStamp参数,DOMHighResTimeStamp指示当前被requestAnimationFrame()排序的回调函数被触发的时间。在同一个帧中的多个回调函数,他们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些事件。该时间戳是一个十进制数,单位是毫秒,最小精度是1ms

WARNING

请确保总是使用第一个参数(或其他获得当前事件的方法)计算每次调用之间的时间间隔,否则动画在高刷新率的屏幕中会运行的更快,请参考下面里的做法

# 语法

TIP

window.requestAnimationFrame(callback);

# 参数
  • callback

下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame()开始去执行回调函数的时刻。

  • 返回值

一个long正数,请求ID,是返回列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给window.cancelAnimationFrame()以取消回调函数。

# 范例
const element = document.getElementById('some-element-you-want-to-animate');
let start;

function step(timestamp) {
    if (start === undefined) {
        start = timestamp;
    }
    const elapased = timestamp - start;

    // 这里使用`Math.min()`确保元素刚好停在200px的位置
    element.style.transform = `translateX(${Math.min(0.1 * elapsed, 200)}px)`;

    // 在两秒后停止动画
    if (elapsed < 2000) {
        window.requestAnimationFrame(step);
    }
}

window.requestAnimationFrame(step);

# Element.animate()

Element接口有animate()方法是一个创建新Animation的便捷方法,将它应用于元素,然后运行动画。它将返回一个新建的Animation对象实例

var animation = element.animate(keyframes, options)

# 示例

在示例 Down the Rabbit Hole (with the Web Animation API) (opens new window) 中, 我们用 animate() 来快速创建并运行使 #tunnel 元素无限循环缓慢升起的动画。注意关键帧的对象数组和时间可选项

document.getElementById('tunnel').animate([
    { transform: 'translateY(0px)' },
    { transform: 'translateY(-300px)'}
], {
    duration: 1000,
    iterations: Infinity
})

Element.animate() (opens new window)

# FLIP

通常,我们使用的Web应用大多数都只是简单的从一个视图切换到另一个树图,导致页面体验不直观,但是我们可以通过技术的手段把这方便的及哦啊胡做的更得体:

在创建UI时,添加合理的UI过渡动效,避免跳转和瞬间移动。如果将生活中的一些自然运动用到UI动效中来,将会给你的用户带来眼前一亮的感觉。毕竟,所有与你互动的东西都源于生活中自然的运动

接下来,我们将一起探讨你可能熟悉的某一类有意义的增强用户体验的UI动效。这种技术有一个专业术语:FLIP(First, Last, Invert, Play).FLIP技术可以以一种高性能的方式来动态的改变DOM元素的位置和尺寸,而不需要管它的布局是如何计算或渲染的(比如,height、width、float、绝对定位、Flexbox和Grid等),在改变的过程中赋予一定的动效,从而达到我们所需要的目的,让UI动效更合理,相应增强用户的体验。

# FLIP是什么

FLIP是一种记忆设备和技术,最早是由@Paul Lewis提出的,FLIP是First、Last、Invert和Play四个单词字母的缩写

// 获取当前元素边界
const first = el.getBoundingClientRect();
// 通过给元素添加一个类名,设置元素最后状态的位置和大小(在.totes-at-the-end中添加相应的样式规则) // 布局发生了变化
el.classList.add('totes-at-the-end'); // 记录元素最后状态的位置和尺寸大小
const last = el.getBoundingChientRect();
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;
elm.animate([ 
    { transformOrigin: 'top left', transform: ` translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH}) ` },
    { transformOrigin: 'top left', transform: 'none' }
], { duration: 300, easing: 'ease-in-out', fill: 'both' } );

为了便于大家更好的理解FLIP技术制作的动效原理,借用下图向大家展示,或许更易于理解: FLIP

加上最后一个过程Play,实现的动画效果如下图所示:

play

前端动画之FLIP技术 (opens new window)

JS动画三剑客——setTimeout、setInterval、requestAnimationFrame (opens new window)