原文連接: 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/…