要實現60FPS動畫, 你須要瞭解這些

原文連接: github.com/yinxin630/b…
技術交流: fiora.suisuijiang.com/javascript

瀏覽器渲染過程

60FPS, 即每秒渲染60幀, 每一幀的間隔時間爲 1000ms / 60 = 16.666mscss

在一次渲染過程當中, 要經歷一下過程: html

image

  • JavaScript: 執行 JavaScript 來觸發一些視覺變化的效果
  • Style: 計算元素匹配的 css 選擇器, 應用各規則計算元素的最終樣式
  • Layout: 根據元素的樣式, 計算元素佔據的空間大小和在屏幕中所處的位置
  • Paint: 向元素的可視部分填充像素, 包括文本 / 圖像 / 邊框 / 陰影, 繪製通常是在多個層上完成的
  • Composite: 將不一樣的層按正確的順序繪製到屏幕上

要保證60FPS, 須要在 16ms 的時間內完成上述過程java

使用 Chrome devtools 分析渲染性能

工欲善其事, 必先利其器. 首先要有工具可以分析性能表現和瓶頸
打開 Chrome devtools 的 Performance 面板, 點擊按鈕或者使用快捷鍵(CMD + E)開始記錄性能react

image

下面經過一個簡單的例子, 來觀察上述渲染過程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

image
在本次繪製過程當中, 共消耗時間 0.63ms + 1.04ms = 1.67ms, 其中 JavaScript 和 Paint 階段耗時較多

另外還有一個查看實時 FPS 的工具, 打開 More tools => Rendering, 勾選 FPS meterweb

image
image

使用 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, 咱們來分析一下每一幀的繪製過程 架構

image
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

利用硬件加速優化 CSS 動畫

使用硬件加速是很簡單的, 只須要把動畫中變化的屬性, 從 margin-left 改成 transform 便可

@keyframes animate {
    from {
        transform: translateX(0px);
    }
    to {
        transform: translateX(400px);
    }
}
複製代碼

觀察性能圖, 主線程徹底空閒了!!

image

使用硬件加速後, 繪製過程將再也不佔用主線程, 直接在 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會掉一次幀

image

timer 是固定間隔時間觸發的, 每過一段時間就會出如今一幀內 timer 觸發兩次的狀況

並且一樣的, JS動畫也是會被主線程阻塞的

使用 requestAnimationFrame 優化 JS 動畫

在高幀率狀況下, setIntervalrequestAnimationFrame 並無明顯的區別, 咱們來增長單幀內的計算量, 首先看 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

總結

  1. 一個繪製過程分爲 JavaScript / Style / Layout / Paint / Composite 五個階段
  2. CSS 動畫若是用了硬件加速, 會將全部繪製過程都放在 GPU 上執行, 不受主線程卡頓影響
  3. 沒用硬件加速的 CSS 動畫, 仍須要在主線程上完成繪製過程
  4. JS 動畫, 用 requestAnimationFrame 會比 setInterval 效果更好
  5. Element.animate() 能夠用 JS 建立和 CSS 同樣效果的動畫, 可是還處於實驗狀態, 兼容性較差
  6. requestIdleCallback 能夠切割長任務, 避免主線程長時間阻塞

參考內容

developers.google.com/web/fundame… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/…

相關文章
相關標籤/搜索