60FPS, 即每秒渲染60帧, 每一帧的间隔时间为 1000ms / 60 = 16.666ms
在一次渲染过程中, 要经历一下过程:
JavaScript
: 执行 JavaScript 来触发一些视觉变化的效果Style
: 计算元素匹配的 css 选择器, 应用各规则计算元素的最终样式Layout
: 根据元素的样式, 计算元素占据的空间大小和在屏幕中所处的位置Paint
: 向元素的可视部分填充像素, 包括文本 / 图像 / 边框 / 阴影, 绘制一般是在多个层上完成的Composite
: 将不同的层按正确的顺序绘制到屏幕上要保证60FPS, 需要在 16ms 的时间内完成上述过程
工欲善其事, 必先利其器. 首先要有工具能够分析性能表现和瓶颈 打开 Chrome devtools 的 Performance 面板, 点击按钮或者使用快捷键(CMD + E)开始记录性能
image
下面通过一个简单的例子, 来观察上述渲染过程
<!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>
打开页面, 开启性能分析, 点击按钮, 停止性能分析并查看结果, 如图所示
在本次绘制过程中, 共消耗时间 0.63ms + 1.04ms = 1.67ms, 其中 JavaScript 和 Paint 阶段耗时较多
另外还有一个查看实时 FPS 的工具, 打开 More tools => Rendering, 勾选 FPS meter
首先基于 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 动画省略了 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
使用硬件加速是很简单的, 只需要把动画中变化的属性, 从 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会掉一次帧
image
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
https://developers.google.com/web/fundamentals/performance/rendering/?hl=zh-cnhttps://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFramehttps://developer.mozilla.org/zh-CN/docs/Web/API/Element/animatehttps://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback