要实现60FPS动画, 你须要了解这些

原文连接: github.com/yinxin630/b…
技术交流: fiora.suisuijiang.com/javascript

浏览器渲染过程

60FPS, 即每秒渲染60帧, 每一帧的间隔时间为 1000ms / 60 = 16.666mscss

在一次渲染过程当中, 要经历一下过程: html

image

  • JavaScript: 执行 JavaScript 来触发一些视觉变化的效果
  • Style: 计算元素匹配的 css 选择器, 应用各规则计算元素的最终样式
  • Layout: 根据元素的样式, 计算元素占据的空间大小和在屏幕中所处的位置
  • Paint: 向元素的可视部分填充像素, 包括文本 / 图像 / 边框 / 阴影, 绘制通常是在多个层上完成的
  • Composite: 将不一样的层按正确的顺序绘制到屏幕上

要保证60FPS, 须要在 16ms 的时间内完成上述过程java

使用 Chrome devtools 分析渲染性能

工欲善其事, 必先利其器. 首先要有工具可以分析性能表现和瓶颈
打开 Chrome devtools 的 Performance 面板, 点击按钮或者使用快捷键(CMD + E)开始记录性能react

image

下面经过一个简单的例子, 来观察上述渲染过程git

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style> div { background-color: red; width: 100px; height: 100px; } </style>
</head>
<body>
    <div></div>
    <button>click</button>
    <script> document.querySelector('button').onclick = () => { document.querySelector('div').style.marginLeft = '100px'; } </script>
</body>
</html>
复制代码

打开页面, 开启性能分析, 点击按钮, 中止性能分析并查看结果, 如图所示 github

image
在本次绘制过程当中, 共消耗时间 0.63ms + 1.04ms = 1.67ms, 其中 JavaScript 和 Paint 阶段耗时较多

另外还有一个查看实时 FPS 的工具, 打开 More tools => Rendering, 勾选 FPS meterweb

image
image

使用 CSS 动画

首先基于 margin-left 属性实现位移动画, 用 position + left 也行浏览器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style> @keyframes animate { from { margin-left: 0px; } to { margin-left: 400px; } } div { background-color: red; width: 100px; height: 100px; animation: animate 2s infinite linear; } </style>
</head>
<body>
    <div></div>
</body>
</html>
复制代码

该动画能够稳定60FPS, 咱们来分析一下每一帧的绘制过程 架构

image
CSS 动画省略了 JavaScript 执行耗时, 只用了 0.49ms 的时间就完成了一帧的绘制

接下来思考一个问题, 若是主线程被阻塞了, CSS动画会有什么表现呢?
<body> 中添加以下代码

<button>block</button>
<script> document.querySelector('button').onclick = () => { for (let i = 0; i < 3000; i++) { console.log(i); } } </script>
复制代码

点击按钮阻塞主线程, JavaScript 代码执行了 264.18ms, 在执行过程当中动画一直卡顿中, 而且卡顿结束会跳帧, 而不是基于卡顿前的位置继续绘制动画

image

利用硬件加速优化 CSS 动画

使用硬件加速是很简单的, 只须要把动画中变化的属性, 从 margin-left 改成 transform 便可

@keyframes animate {
    from {
        transform: translateX(0px);
    }
    to {
        transform: translateX(400px);
    }
}
复制代码

观察性能图, 主线程彻底空闲了!!

image

使用硬件加速后, 绘制过程将再也不占用主线程, 直接在 GPU 上完成
所以, 点击按钮阻塞主线程, 也并不会影响动画, 你能够亲自试一试

使用 JS 动画

首先使用 setInterval 实现动画循环

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style> div { background-color: red; width: 100px; height: 100px; } </style>
</head>
<body>
    <div></div>
    <button>block</button>
    <script> window.onload = () => { const $div = document.querySelector('div'); let left = 0; setInterval(() => { left += 5; if (left > 400) { left = 0; } $div.style.marginLeft = left + 'px'; }, 1000 / 60); } document.querySelector('button').onclick = () => { for (let i = 0; i < 3000; i++) { console.log(i); } } </script>
</body>
</html>
复制代码

观察此时的 FPS 帧率, 大约每隔10s会掉一次帧

image

timer 是固定间隔时间触发的, 每过一段时间就会出如今一帧内 timer 触发两次的状况

并且一样的, JS动画也是会被主线程阻塞的

使用 requestAnimationFrame 优化 JS 动画

在高帧率状况下, setIntervalrequestAnimationFrame 并无明显的区别, 咱们来增长单帧内的计算量, 首先看 setInterval

function work() {
    for (let i = 0; i < 100000000; i++) {}

    left += 5;
    if (left > 400) {
        left = 0;
    }
    $div.style.marginLeft = left + 'px';
}
setInterval(work, 1000 / 60);
复制代码

此时的 FPS 大约在 18 左右(受机器性能影响)
那么换成 requestAnimationFrame 呢?

function work() {
    for (let i = 0; i < 100000000; i++) {}

    left += 5;
    if (left > 400) {
        left = 0;
    }
    $div.style.marginLeft = left + 'px';
    requestAnimationFrame(work);
}
work();
复制代码

此时的 FPS 稳定在 31 左右, 相同的 work 方法, 在使用 requestAnimationFrame 时比会 setInterval 耗时更少
requestAnimationFrame 会确保回调在一帧开始时触发

使用 Element.animate() 建立支持硬件加速的动画

Element.animate() 仍是一个实验中的功能, Chrome 最先在 36 版本中就实现了其基础功能
使用 Element.animate() 能够便捷的建立动画, 而且像 CSS 动画同样, 具备调用硬件加速的能力

const $div = document.querySelector('div');
$div.animate(
    [
        { transform: 'translateX(0px)' },
        { transform: 'translateX(400px)' },
    ],
    {
        duration: 2000,
        iterations: Infinity
    }
)
复制代码

使用 requestIdleCallback 避免主线程阻塞

无论怎么样, 长时间占用主线程都是一种不好的操做, 在阻塞期间, 动画卡顿, 用户操做事件没法响应, 咱们要避免长时间阻塞的行为
如何避免呢? 能够将长任务划分为一个个短任务, 在主线程空闲时, 按顺序一个个执行. 怎么知道主线程是否空闲呢? requestIdleCallback 就是咱们想要的
requestIdleCallback 接收一个 callback 函数做为参数, 会在主线程空闲时, 按注册顺序逐个执行 callback

将 block 按钮用 requestIdleCallback 重写

document.querySelector('button').onclick = () => {
    let a = 0;
    for (let i = 0; i < 30; i++) {
        requestIdleCallback(() => {
            for (let j = 0; j < 100; j++) {
                console.log(a);
                a++;
            }
        })
    }
}
复制代码

这里将任务分红 30 组, 每组调用一次 requestIdleCallback, 这时候再点击按钮, 动画就不会卡顿了

react 的 fiber 架构也是基于 requestIdleCallback 实现的, 而且在不支持的浏览器中提供了 polyfill

总结

  1. 一个绘制过程分为 JavaScript / Style / Layout / Paint / Composite 五个阶段
  2. CSS 动画若是用了硬件加速, 会将全部绘制过程都放在 GPU 上执行, 不受主线程卡顿影响
  3. 没用硬件加速的 CSS 动画, 仍须要在主线程上完成绘制过程
  4. JS 动画, 用 requestAnimationFrame 会比 setInterval 效果更好
  5. Element.animate() 能够用 JS 建立和 CSS 同样效果的动画, 可是还处于实验状态, 兼容性较差
  6. requestIdleCallback 能够切割长任务, 避免主线程长时间阻塞

参考内容

developers.google.com/web/fundame… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/…

相关文章
相关标签/搜索