原文连接: github.com/yinxin630/b…
技术交流: fiora.suisuijiang.com/javascript
60FPS, 即每秒渲染60帧, 每一帧的间隔时间为 1000ms / 60 = 16.666mscss
在一次渲染过程当中, 要经历一下过程: html
JavaScript
: 执行 JavaScript 来触发一些视觉变化的效果Style
: 计算元素匹配的 css 选择器, 应用各规则计算元素的最终样式Layout
: 根据元素的样式, 计算元素占据的空间大小和在屏幕中所处的位置Paint
: 向元素的可视部分填充像素, 包括文本 / 图像 / 边框 / 阴影, 绘制通常是在多个层上完成的Composite
: 将不一样的层按正确的顺序绘制到屏幕上要保证60FPS, 须要在 16ms 的时间内完成上述过程java
工欲善其事, 必先利其器. 首先要有工具可以分析性能表现和瓶颈
打开 Chrome devtools 的 Performance 面板, 点击按钮或者使用快捷键(CMD + E)开始记录性能react
下面经过一个简单的例子, 来观察上述渲染过程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
另外还有一个查看实时 FPS 的工具, 打开 More tools => Rendering, 勾选 FPS meterweb
首先基于 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, 咱们来分析一下每一帧的绘制过程 架构
接下来思考一个问题, 若是主线程被阻塞了, CSS动画会有什么表现呢?
在 <body>
中添加以下代码
<button>block</button>
<script> document.querySelector('button').onclick = () => { for (let i = 0; i < 3000; i++) { console.log(i); } } </script>
复制代码
点击按钮阻塞主线程, JavaScript 代码执行了 264.18ms, 在执行过程当中动画一直卡顿中, 而且卡顿结束会跳帧, 而不是基于卡顿前的位置继续绘制动画
使用硬件加速是很简单的, 只须要把动画中变化的属性, 从 margin-left
改成 transform
便可
@keyframes animate {
from {
transform: translateX(0px);
}
to {
transform: translateX(400px);
}
}
复制代码
观察性能图, 主线程彻底空闲了!!
使用硬件加速后, 绘制过程将再也不占用主线程, 直接在 GPU 上完成
所以, 点击按钮阻塞主线程, 也并不会影响动画, 你能够亲自试一试
首先使用 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会掉一次帧
timer 是固定间隔时间触发的, 每过一段时间就会出如今一帧内 timer 触发两次的状况
并且一样的, JS动画也是会被主线程阻塞的
在高帧率状况下, setInterval
和 requestAnimationFrame
并无明显的区别, 咱们来增长单帧内的计算量, 首先看 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()
仍是一个实验中的功能, Chrome 最先在 36 版本中就实现了其基础功能
使用 Element.animate()
能够便捷的建立动画, 而且像 CSS 动画同样, 具备调用硬件加速的能力
const $div = document.querySelector('div');
$div.animate(
[
{ transform: 'translateX(0px)' },
{ transform: 'translateX(400px)' },
],
{
duration: 2000,
iterations: Infinity
}
)
复制代码
无论怎么样, 长时间占用主线程都是一种不好的操做, 在阻塞期间, 动画卡顿, 用户操做事件没法响应, 咱们要避免长时间阻塞的行为
如何避免呢? 能够将长任务划分为一个个短任务, 在主线程空闲时, 按顺序一个个执行. 怎么知道主线程是否空闲呢? 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
developers.google.com/web/fundame… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/…