前端動(dòng)畫實(shí)現(xiàn)以及原理淺析

背景

如今的前端是一個(gè)涉獵領(lǐng)域很廣的職業(yè)。作為一名前端,我們不僅要開發(fā)管理系統(tǒng)、數(shù)據(jù)中臺(tái)、還要應(yīng)對(duì)年報(bào)開發(fā)、節(jié)日活動(dòng)等場(chǎng)景。不僅要會(huì)增刪改查,編寫表單,還要具備開發(fā)動(dòng)畫、H5 游戲等能力。能做出很 Cool 的動(dòng)畫效果,也是一種前端特有的成就感。所以,我們從動(dòng)畫的實(shí)現(xiàn)方法入手,了解瀏覽器的渲染,以及如何提升動(dòng)畫的性能。

我們先來看 2 個(gè) H5 案例 :
【一鏡到底】

file

【年報(bào)】

file

file

file

【其他 H5 優(yōu)秀案例】
https://www.h5anli.com

第一部分 常見的動(dòng)畫實(shí)現(xiàn)手段

1.1 gif 實(shí)現(xiàn)

file

定義:
GIF 文件的數(shù)據(jù)是一種基于 LZW 算法的連續(xù)色調(diào)的無損壓縮格式,gif 格式的特點(diǎn)是一個(gè) gif 文件可以存多幅彩色圖像,當(dāng)數(shù)據(jù)逐幅讀出并展示都在屏幕上,就可以構(gòu)成一個(gè)簡(jiǎn)單的動(dòng)畫。
最高支持 256 種顏色。由于這種特性,GIF 比較適用于色彩較少的圖片,比如頁(yè)面卡通 icon、標(biāo)志等等。

使用:

![file](https://upload-images.jianshu.io/upload_images/26021660-b16480773fd84b1b.png)

優(yōu)點(diǎn):
1.制作的成本很低;
2.兼容性好;
3.方便開發(fā)使用。

缺點(diǎn):
1.畫質(zhì)上:色彩支持少,圖像毛邊嚴(yán)重;
2.交互上:不能控制動(dòng)畫的播放暫停,沒有靈活性;
3.大小上:由于是無損壓縮,每幀被完整的保存下來,導(dǎo)致文件很大。

1.2 css3 補(bǔ)幀動(dòng)畫

1.2.1 transition 過渡動(dòng)畫

使用:

.box {
    border: 1px solid black;
    width: 100px;
    height: 100px;
    background-color: #0000ff;
    transition: width 2s, height 2s, background-color 2s, transform 2s;
}

.box:hover {
    background-color: #ffcccc;
    width: 200px;
    height: 200px;
    transform: rotate(180deg);
}

場(chǎng)景:

常與 :hover, :active 等偽類使用,實(shí)現(xiàn)相應(yīng)等動(dòng)畫效果。

1.2.2 animation 關(guān)鍵幀動(dòng)畫

使用:

.bounce1 {
    left: -40px;
    animation: bouncy1 1.5s infinite;
}
.bounce2 {
    left: 0;
    animation: bouncy2 1.5s infinite;
}
.bounce3 {
    left: 40px;
    animation: bouncy3 1.5s infinite;
}
@keyframes bouncy1 {
    0% {
        transform: translate(0px, 0px) rotate(0deg);
    }
    50% {
        transform: translate(0px, 0px) rotate(180deg);
    }
    100% {
        transform: translate(40px, 0px) rotate(-180deg);
    }
}

場(chǎng)景:
比如:loading 展示,代碼如上。

優(yōu)點(diǎn):
1、無需每一幀都被記錄,通過關(guān)鍵幀設(shè)置,方便開發(fā);
2.實(shí)現(xiàn)簡(jiǎn)單,通常 UI 可以直接給到 css 文件,前端只需要導(dǎo)入即可【移動(dòng)端注意屏幕適配】。

缺點(diǎn):
1.css 沒法動(dòng)畫交互,無法得知當(dāng)前動(dòng)畫執(zhí)行階段;
2.transition: 需要觸發(fā),無法自動(dòng)播放;
3.animation 兼容性需要加前綴,導(dǎo)致代碼量成倍增長(zhǎng);
4.對(duì)于復(fù)雜動(dòng)畫的實(shí)現(xiàn),導(dǎo)入的 css 文件過大,影響頁(yè)面的渲染樹生成,從而阻塞渲染。比如實(shí)現(xiàn)一個(gè)搖錢樹的效果,css 文件達(dá)到百 kb,就要采取一些必要的壓縮手段,縮減文件大小。

1.3 js 逐幀動(dòng)畫

JS 動(dòng)畫的原理是通過 setTimeout 或 requestAnimationFrame 方法繪制動(dòng)畫幀,從而動(dòng)態(tài)地改變
網(wǎng)頁(yè)中圖形的顯示屬性(如 DOM 樣式,canvas 位圖數(shù)據(jù),SVG 對(duì)象屬性等),進(jìn)而達(dá)到動(dòng)畫的目的。

demo1:
------- js 實(shí)現(xiàn)一個(gè)正方形從左到右的移動(dòng)動(dòng)畫 -----

  1. setTimeout 實(shí)現(xiàn)
const element2 = document.getElementById('raf2');
const btn2 = document.getElementById('btn2');
let i = 0;
let timerId;
function move () {   
    element2.style.marginLeft = i + 'px'
    timerId = setTimeout(move, 0)
    i++;
    if (i > 200) {
        clearTimeout(timerId)
    }
}
btn2.addEventListener('click',function () {
    move()
})
  1. requestAnimationFrame 實(shí)現(xiàn)
const element = document.getElementById('raf');
const btn1 = document.getElementById('btn1');
let r = 0;
let rafId;
function step () {
    element1.style.marginLeft = r+ 'px';
    rafId = window.requestAnimationFrame(step);
    r++;
    if (r > 200) { // 在兩秒后停止動(dòng)畫
        cancelAnimationFrame(rafId);
    }
}
btn1.addEventListener('click', function () {
    step();
})

可以看出,實(shí)現(xiàn)的方式都是控制 dom 的 margin-left 樣式,執(zhí)行動(dòng)畫。

問題 1.1:demo1 中看出 setTimeout 的執(zhí)行很快。這是為什么呢?請(qǐng)接著往后看~

第二部分 瀏覽器如何渲染與動(dòng)畫的渲染

2.1 瀏覽器的幀原理

問題 2:當(dāng) url 輸入到一個(gè)頁(yè)面展示出來經(jīng)過了哪些過程?

這里我們忽略 http 請(qǐng)求靜態(tài)文件之前的步驟,著重看瀏覽器渲染幀是怎么做的,從而找到瀏覽器是如何渲染動(dòng)畫的。

file

借助 chrome-performance【執(zhí)行 raf.html】 同樣可以看出上圖不同階段在 performance 里面的標(biāo)注。??注意:不是每幀都總是會(huì)經(jīng)過管道每個(gè)部分的處理。實(shí)際上,不管是使用 JavaScript、CSS 還是網(wǎng)絡(luò)動(dòng)畫,在實(shí)現(xiàn)視覺變化時(shí),管道幀對(duì)指定幀的運(yùn)行通常有三種方式:

【以下截圖是以時(shí)間線為主軸,進(jìn)行繪制】

file

-當(dāng)修改一些會(huì)觸發(fā) layout 的屬性,則會(huì)導(dǎo)致后面的同樣被更新。

file
  • 當(dāng)修改只改變 paint 的屬性, 則不會(huì)重新 layout。
file
  • 如果改一些不涉及布局也不涉及重繪的數(shù)據(jù),則可以直接進(jìn)行合成渲染。

像 CSS 屬性具體可以查詢這個(gè)網(wǎng)站 ,去查閱哪些屬性會(huì)引起怎樣的幀管道:https://csstriggers.com/

例如:transform 變換,它是一個(gè)不會(huì)觸發(fā)布局與繪制的變化的,所以使用它的時(shí)候,直接進(jìn)入第三種狀態(tài),在合成之后,直接進(jìn)入 Composite 階段,是一個(gè)很好的優(yōu)化手段。

file

問題 3: 控制臺(tái)上顯示出 requestAnimationFrame(rAF)的執(zhí)行,那么這個(gè) rAF 執(zhí)行與瀏覽器幀有什么關(guān)系呢?我們接著往下看。

2.2 requestAnimationFrame 執(zhí)行

我們還是運(yùn)行 demo1 的代碼:

可以看到 rAF 執(zhí)行在 layout 與 paint 之前,在每幀只執(zhí)行了一次 rAF,調(diào)用回調(diào)函數(shù)執(zhí)行動(dòng)畫。

從 rAF 的執(zhí)行時(shí)機(jī),可以看出 setTimeout 的執(zhí)行時(shí)機(jī)與 rAF 的不同。

我們通過對(duì)不同方式實(shí)現(xiàn)方塊移動(dòng)動(dòng)畫的 performance 抓取,可以看到:

  • setTimeout 單位幀截圖:
file
  • rAF 單位幀截圖:
file

對(duì)比兩者可以看出,在 16.7ms 的時(shí)間里,seTimeout 執(zhí)行了 4 次,導(dǎo)致此時(shí)設(shè)置的 marginLeft 和上一次渲染前 marginLeft 的差值要大于 1px 的。

而 raf 可以看出 marginLeft 和上一次渲染前 marginLeft 的差值要等于 1px 的。

從 rAf 的性能,可以看出 setTimeout 的性能會(huì)較差一點(diǎn)

那么如果 JS 執(zhí)行的時(shí)間過長(zhǎng),導(dǎo)致在本該繪制一幀的時(shí)候,沒有繪制,延遲到下一幀的執(zhí)行繪制的時(shí)候,就會(huì)造成動(dòng)畫的卡頓?!具@里可以跳到第三部分性能問題,就知道直觀的看到卡頓】

從而可以總結(jié)出:

1.setTimeout 時(shí)間不準(zhǔn)確,因?yàn)樗膱?zhí)行取決于主線程執(zhí)行的時(shí)間。

2.如果計(jì)時(shí)器頻率高于瀏覽器刷新的頻率,即使代碼執(zhí)行了,瀏覽器沒有刷新,也是沒有顯示的,出現(xiàn)掉幀情況,不流暢。

而 raf 解決了 setTimeout 動(dòng)畫帶來的問題:

1.瀏覽器刷新屏幕時(shí)自動(dòng)執(zhí)行,無需設(shè)置時(shí)間間隔
和 setTimeout 一樣是 n 毫秒之后再執(zhí)行,但這個(gè) n 毫秒,自動(dòng)設(shè)置成瀏覽器刷新頻率,瀏覽器刷新一次,執(zhí)行一次,不需要手動(dòng)設(shè)置;
瀏覽器不刷新,就不執(zhí)行,沒有排隊(duì)掉幀的情況。

2.高頻函數(shù)節(jié)流
對(duì)于 resize、scroll 高頻觸發(fā)事件來說,使用 requestAnimationFrame 可以保證在每個(gè)繪制區(qū)間內(nèi),函數(shù)只被執(zhí)行一次,節(jié)省函數(shù)執(zhí)行的開銷。
如果使用 setTimeout、setInterval 可能會(huì)在瀏覽器刷新間隔中有無用的回調(diào)函數(shù)調(diào)用,浪費(fèi)資源。

第三部分 性能分析以及高效能的動(dòng)畫

3.1 性能分析

通過 chrome-performance 可以看整體的 fps、GPU 的情況,也可以逐幀去分析影響 scripting\rendering\painting 時(shí)間的因素,從而有針對(duì)性的提高動(dòng)畫的性能。

demo3:
----- 小方塊的上下運(yùn)動(dòng) -----

demo 的在線地址:https://googlechrome.github.io/devtools-samples/jank/

源碼截圖:

file

未優(yōu)化【每個(gè)方塊都需要強(qiáng)制 layout 去計(jì)算 position】:

file

點(diǎn)擊 Optimize 按鈕優(yōu)化后【只讀一次,并存在 pos 變量中】:

file

再次優(yōu)化【添加 transform:translateZ(0),提高層級(jí)】:

file

以上就是一個(gè)動(dòng)畫逐步優(yōu)化的小案例:具體操作可以查看原文:
https://developer.chrome.com/docs/devtools/evaluate-performance/

3.2 如何優(yōu)化動(dòng)畫性能

根據(jù)上文的渲染機(jī)制的討論,我們可以看出,影響動(dòng)畫渲染的因素就是幀管道所經(jīng)歷的各個(gè)階段,從中我們可以總結(jié)一些用來優(yōu)化動(dòng)畫性能的手段:

  1. 提升每一幀的性能
  • 避免頻繁的重排
  • 避免大面積的重繪
  • 優(yōu)化 JS 的性能
  1. fps 穩(wěn)定,避免掉幀,跳幀的情況
  • 不在連續(xù)動(dòng)畫中,添加高耗能的操作
  • 如果無法避免,看可以在動(dòng)畫的開頭或者結(jié)尾進(jìn)行操作
  1. 開啟 GUP 加速

第四部分 常用的動(dòng)畫庫(kù)

綜上的實(shí)現(xiàn)方式可以支持部分的動(dòng)畫開發(fā),比如點(diǎn)擊交互,輪播器、以及純動(dòng)畫的展示,比如搖錢樹、煙花等。

如果需要強(qiáng)交互,或者是需要一個(gè)重力世界的時(shí)候,原生 JS 的實(shí)現(xiàn)相對(duì)于困難。可以利用一些動(dòng)畫庫(kù),來進(jìn)行開發(fā),這些動(dòng)畫基于 canvas 與 webGL 實(shí)現(xiàn)的。

  1. Pixi.js
  • 添加場(chǎng)景
  • 添加玩家
  • 添加自身動(dòng)作
  • 添加交互
  1. phaser.js
    物理系統(tǒng)、重力系統(tǒng)
    可以模仿下落狀態(tài)

  2. 其他:
    create.js、three.js 3d 渲染、layaAir、Egret 3d 游戲引擎等,可以根據(jù)不同的場(chǎng)景需要,選擇不同的框架使用。

總結(jié)

  1. 動(dòng)畫的實(shí)現(xiàn)手段
file
  1. 瀏覽器渲染的簡(jiǎn)單流程
file
  1. 開發(fā)動(dòng)畫分析性能參考 performance 的使用

鳴謝

非常感謝木杪、千尋對(duì)本文的校正與建議,同時(shí)感謝琉易、霜序等伙伴在業(yè)務(wù)產(chǎn)品技術(shù)上幫助與支持。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容