60FPS, 即每秒渲染60幀, 每一幀的間隔時間爲 1000ms / 60 = 16.666mscss
在一次渲染過程當中, 要經歷一下過程:html
-
JavaScript
: 執行 JavaScript 來觸發一些視覺變化的效果 -
Style
: 計算元素匹配的 css 選擇器, 應用各規則計算元素的最終樣式 -
Layout
: 根據元素的樣式, 計算元素佔據的空間大小和在屏幕中所處的位置 -
Paint
: 向元素的可視部分填充像素, 包括文本 / 圖像 / 邊框 / 陰影, 繪製通常是在多個層上完成的 -
Composite
: 將不一樣的層按正確的順序繪製到屏幕上
要保證60FPS, 須要在 16ms 的時間內完成上述過程react
使用 Chrome devtools 分析渲染性能
工欲善其事, 必先利其器. 首先要有工具可以分析性能表現和瓶頸
打開 Chrome devtools 的 Performance 面板, 點擊按鈕或者使用快捷鍵(CMD + E)開始記錄性能web

下面經過一個簡單的例子, 來觀察上述渲染過程瀏覽器
<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架構
使用 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, 咱們來分析一下每一幀的繪製過程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
, 在執行過程當中動畫一直卡頓中, 而且卡頓結束會跳幀, 而不是基於卡頓前的位置繼續繪製動畫

利用硬件加速優化 CSS 動畫
使用硬件加速是很簡單的, 只須要把動畫中變化的屬性, 從 margin-left
改成 transform
便可
@keyframes animate { from { transform: translateX(0px); } to { transform: translateX(400px); }}
觀察性能圖, 主線程徹底空閒了!!
使用硬件加速後, 繪製過程將再也不佔用主線程, 直接在 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會掉一次幀

timer 是固定間隔時間觸發的, 每過一段時間就會出如今一幀內 timer 觸發兩次的狀況
並且一樣的, JS動畫也是會被主線程阻塞的
使用 requestAnimationFrame 優化 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() 建立支持硬件加速的動畫
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
總結
-
一個繪製過程分爲 JavaScript / Style / Layout / Paint / Composite 五個階段 -
CSS 動畫若是用了硬件加速, 會將全部繪製過程都放在 GPU 上執行, 不受主線程卡頓影響 -
沒用硬件加速的 CSS 動畫, 仍須要在主線程上完成繪製過程 -
JS 動畫, 用 requestAnimationFrame 會比 setInterval 效果更好 -
Element.animate() 能夠用 JS 建立和 CSS 同樣效果的動畫, 可是還處於實驗狀態, 兼容性較差 -
requestIdleCallback 能夠切割長任務, 避免主線程長時間阻塞
參考內容
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
本文分享自微信公衆號 - 牧碼的星星(gh_0d71d9e8b1c3)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。