We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
60FPS, 即每秒渲染60帧, 每一帧的间隔时间为 1000ms / 60 = 16.666ms
在一次渲染过程中, 要经历一下过程:
JavaScript
Style
Layout
Paint
Composite
要保证60FPS, 需要在 16ms 的时间内完成上述过程
工欲善其事, 必先利其器. 首先要有工具能够分析性能表现和瓶颈 打开 Chrome devtools 的 Performance 面板, 点击按钮或者使用快捷键(CMD + E)开始记录性能
下面通过一个简单的例子, 来观察上述渲染过程
<!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 也行
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> 中添加如下代码
<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 即可
transform
@keyframes animate { from { transform: translateX(0px); } to { transform: translateX(400px); } }
观察性能图, 主线程完全空闲了!!
使用硬件加速后, 绘制过程将不再占用主线程, 直接在 GPU 上完成 因此, 点击按钮阻塞主线程, 也并不会影响动画, 你可以亲自试一试
首先使用 setInterval 实现动画循环
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
requestAnimationFrame
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 动画一样, 具有调用硬件加速的能力
Element.animate()
const $div = document.querySelector('div'); $div.animate( [ { transform: 'translateX(0px)' }, { transform: 'translateX(400px)' }, ], { duration: 2000, iterations: Infinity } )
不管怎么样, 长时间占用主线程都是一种很差的操作, 在阻塞期间, 动画卡顿, 用户操作事件无法响应, 我们要避免长时间阻塞的行为 如何避免呢? 可以将长任务划分为一个个短任务, 在主线程空闲时, 按顺序一个个执行. 怎么知道主线程是否空闲呢? requestIdleCallback 就是我们想要的 requestIdleCallback 接收一个 callback 函数作为参数, 会在主线程空闲时, 按注册顺序逐个执行 callback
requestIdleCallback
将 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-cn https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame https://developer.mozilla.org/zh-CN/docs/Web/API/Element/animate https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
The text was updated successfully, but these errors were encountered:
很好的一篇文章,通过阅读这篇文章对60FPS动画有了全面认识,总结的很全面!🎉🎉🎉🎉值得我们所有人去学习!🎉🎉🎉🎉🎉
Sorry, something went wrong.
赞
这很细节
非常有用!!
No branches or pull requests
60FPS, 即每秒渲染60帧, 每一帧的间隔时间为 1000ms / 60 = 16.666ms
在一次渲染过程中, 要经历一下过程:
JavaScript
: 执行 JavaScript 来触发一些视觉变化的效果Style
: 计算元素匹配的 css 选择器, 应用各规则计算元素的最终样式Layout
: 根据元素的样式, 计算元素占据的空间大小和在屏幕中所处的位置Paint
: 向元素的可视部分填充像素, 包括文本 / 图像 / 边框 / 阴影, 绘制一般是在多个层上完成的Composite
: 将不同的层按正确的顺序绘制到屏幕上要保证60FPS, 需要在 16ms 的时间内完成上述过程
使用 Chrome devtools 分析渲染性能
工欲善其事, 必先利其器. 首先要有工具能够分析性能表现和瓶颈
打开 Chrome devtools 的 Performance 面板, 点击按钮或者使用快捷键(CMD + E)开始记录性能
下面通过一个简单的例子, 来观察上述渲染过程
打开页面, 开启性能分析, 点击按钮, 停止性能分析并查看结果, 如图所示
在本次绘制过程中, 共消耗时间 0.63ms + 1.04ms = 1.67ms, 其中 JavaScript 和 Paint 阶段耗时较多
另外还有一个查看实时 FPS 的工具, 打开 More tools => Rendering, 勾选 FPS meter
使用 CSS 动画
首先基于
margin-left
属性实现位移动画, 用position + left
也行该动画可以稳定60FPS, 我们来分析一下每一帧的绘制过程
CSS 动画省略了 JavaScript 执行耗时, 只用了 0.49ms 的时间就完成了一帧的绘制
接下来思考一个问题, 如果主线程被阻塞了, CSS动画会有什么表现呢?
在
<body>
中添加如下代码点击按钮阻塞主线程, JavaScript 代码执行了 264.18ms, 在执行过程中动画一直卡顿中, 并且卡顿结束会跳帧, 而不是基于卡顿前的位置继续绘制动画
利用硬件加速优化 CSS 动画
使用硬件加速是很简单的, 只需要把动画中变化的属性, 从
margin-left
改为transform
即可观察性能图, 主线程完全空闲了!!
使用硬件加速后, 绘制过程将不再占用主线程, 直接在 GPU 上完成
因此, 点击按钮阻塞主线程, 也并不会影响动画, 你可以亲自试一试
使用 JS 动画
首先使用
setInterval
实现动画循环观察此时的 FPS 帧率, 大约每隔10s会掉一次帧
timer 是固定间隔时间触发的, 每过一段时间就会出现在一帧内 timer 触发两次的情况
而且同样的, JS动画也是会被主线程阻塞的
使用 requestAnimationFrame 优化 JS 动画
在高帧率情况下,
setInterval
和requestAnimationFrame
并没有明显的区别, 我们来增加单帧内的计算量, 首先看setInterval
此时的 FPS 大约在 18 左右(受机器性能影响)
那么换成
requestAnimationFrame
呢?此时的 FPS 稳定在 31 左右, 相同的 work 方法, 在使用
requestAnimationFrame
时比会setInterval
耗时更少requestAnimationFrame
会确保回调在一帧开始时触发使用 Element.animate() 创建支持硬件加速的动画
Element.animate()
还是一个实验中的功能, Chrome 最早在 36 版本中就实现了其基础功能使用
Element.animate()
可以便捷的创建动画, 并且像 CSS 动画一样, 具有调用硬件加速的能力使用 requestIdleCallback 避免主线程阻塞
不管怎么样, 长时间占用主线程都是一种很差的操作, 在阻塞期间, 动画卡顿, 用户操作事件无法响应, 我们要避免长时间阻塞的行为
如何避免呢? 可以将长任务划分为一个个短任务, 在主线程空闲时, 按顺序一个个执行. 怎么知道主线程是否空闲呢?
requestIdleCallback
就是我们想要的requestIdleCallback
接收一个 callback 函数作为参数, 会在主线程空闲时, 按注册顺序逐个执行 callback将 block 按钮用 requestIdleCallback 重写
这里将任务分成 30 组, 每组调用一次 requestIdleCallback, 这时候再点击按钮, 动画就不会卡顿了
react 的 fiber 架构也是基于 requestIdleCallback 实现的, 并且在不支持的浏览器中提供了 polyfill
总结
参考内容
https://developers.google.com/web/fundamentals/performance/rendering/?hl=zh-cn
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
https://developer.mozilla.org/zh-CN/docs/Web/API/Element/animate
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
The text was updated successfully, but these errors were encountered: