幀動畫的多種實現方式與效能對比
作者:前端向朔 from 迅雷前端
原文地址: 幀動畫的多種實現方式與效能對比
本文目錄
Web動畫形式 應用場景 素材準備 實現方案 一、GIF圖 二、CSS3幀動畫 三、JS幀動畫 主執行緒和排版執行緒 方案總結 注意事項 總結 複製程式碼
Web動畫形式
首先我們來了解一下Web有哪些動畫形式
1. CSS3動畫 Transform(變形) Transition(過渡) Animation(動畫) 2. JS動畫(操作DOM、修改CSS屬性值) 3. Canvas動畫 4. SVG動畫 5. 以Three.js為首的3D動畫 複製程式碼
以上各種動畫形式都可以製作出一種型別的動畫,那就是 幀動畫 ,也叫序列幀動畫,定格動畫,逐幀動畫等,這裡我們統一用幀動畫來表述。

應用場景
幀動畫一般用來實現稍微複雜一點的動畫效果,同時希望動畫更細膩,設計師更自由的發揮。他可以定義到每一個時間刻度上的展現內容,我們一般用幀動畫來做頁面的Loading,小人物,小物體元素的簡單動畫。我們想象中的幀動畫應該有以下幾個特點:
- 可以自由控制播放、暫停和停止
- 可以控制播放次數,播放速度
- 可以新增互動,在播放完成後新增事件
- 瀏覽器相容性好
素材準備
幀動畫的素材一般是先由設計師在PS中的時間軸上設計好了,然後匯出圖片給前端人員,PS製作時間軸動畫一般是用來製作稍微簡單的動畫,操作簡單,方便。
或者是由設計師在AE的時間軸進行設計,因為AE內建了更豐富的動作效果,比如轉換,翻轉之類的,AE可以幫助我們實現更復雜的效果,然後再匯出圖片給前端人員。
這裡幀動畫素材的要求,每一幀的圖片最好是偶數寬高,偶數張,最好周圍能有一些留白。
實現方案
將目前想到的解決方案梳理如下圖,同時我們將對每種方案進行詳細介紹。

一、GIF圖
我們可以將上面製作的幀動畫匯出成GIF圖,GIF圖會連續播放,無法暫停,它往往用來實現小細節動畫,成本較低、使用方便。但其缺點也是很明顯的:
- 畫質上,gif 支援顏色少(最大256色)、Alpha 透明度支援差,影象鋸齒毛邊比較嚴重;
- 互動上,不能直接控制播放、暫停、播放次數,靈活性差;
- 效能上,gif 會引起頁面週期性的 繪畫 ,效能較差。
二、CSS3幀動畫
CSS3幀動畫是我們今天需要重點介紹的方案,最核心的是利用CSS3中 Animation動畫 ,確切的說是使用 animation-timing-function
的階梯函式 steps(number_of_steps, direction)
來實現逐幀動畫的連續播放。
幀動畫的實現原理是不斷切換視覺內圖片內容,利用視覺滯留生理現象來實現連續播放的動畫效果,下面我們來介紹製作CSS3幀動畫的幾種方案。
(1)連續切換動畫圖片地址src(不推薦)
我們將圖片放到元素的背景中( background-image
),通過更改 background-image
的值實現幀的切換。但是這種方式會有以下幾個缺點,所以該方案不推薦。
- 多張圖片會帶來多個 HTTP 請求
- 每張圖片首次載入會造成圖片切換時的閃爍
- 不利於檔案的管理
(2)連續切換雪碧圖位置(推薦)
我們將所有的幀動畫圖片合併成一張雪碧圖,通過改變 background-position
的值來實現動畫幀切換。分兩步進行:
步驟一:將動畫幀合併為雪碧圖,雪碧圖的要求可以看上面 素材準備 ,比如下面這張幀動畫雪碧圖,共20幀。

步驟二:使用steps階梯函式切換雪碧圖位置
先看寫法一:
<div class="sprite"></div> .sprite { width: 300px; height: 300px; background-repeat: no-repeat; background-image: url(frame.png); animation: frame 1s steps(1,end) both infinite; } @keyframes frame { 0% {background-position: 0 0;} 5% {background-position: -300px 0;} 10% {background-position: -600px 0;} 15% {background-position: -900px 0;} 20% {background-position: -1200px 0;} 25% {background-position: -1500px 0;} 30% {background-position: -1800px 0;} 35% {background-position: -2100px 0;} 40% {background-position: -2400px 0;} 45% {background-position: -2700px 0;} 50% {background-position: -3000px 0;} 55% {background-position: -3300px 0;} 60% {background-position: -3600px 0;} 65% {background-position: -3900px 0;} 70% {background-position: -4200px 0;} 75% {background-position: -4500px 0;} 80% {background-position: -4800px 0;} 85% {background-position: -5100px 0;} 90% {background-position: -5400px 0;} 95% {background-position: -5700px 0;} 100% {background-position: -6000px 0;} } 複製程式碼
針對以上動畫有疑問?
問題一:既然都詳細定義關鍵幀了,是不是可以不用steps函數了,直接定義linear變化不就好了嗎?
animation: frame 10s linear both infinite;
如果我們定義成這樣,動畫是不會階梯狀,一步一步執行的,而是會連續的變化背景圖位置,是移動的效果,而不是切換的效果,如下圖:

問題二:不是應該設定為20步嗎,怎麼變成了1?
這裡我們先來了解下 animation-timing-function
屬性。
CSS animation-timing-function
屬性定義CSS動畫在每一動畫週期中執行的節奏。對於關鍵幀動畫來說,timing function作用於一個關鍵幀週期而非整個動畫週期,即從關鍵幀開始開始,到關鍵幀結束結束。
timing-function 作用於每兩個關鍵幀之間,而不是整個動畫。
接著我們來了解下steps() 函式:
- steps 函式指定了一個階躍函式,它接受兩個引數。
- 第一個引數接受一個整數值,表示兩個關鍵幀之間分幾步完成。
- 第二個引數有兩個值< start > or < end >。預設值為< end > 。
- step-start 等同於 step(1, start)。step-end 等同於 step(1, end)。
綜上我們可以知道,因為我們詳細定義了一個關鍵幀週期,從開始到結束,每兩個關鍵幀之間分 1 步展示完,也就是說0% ~ 5%之間變化一次,5% ~ 10%變化一次,所以我們這樣寫才能達到想要的效果。
再看寫法二:
<div class="sprite"></div> .sprite { width: 300px; height: 300px; background-repeat: no-repeat; background-image: url(frame.png); animation: frame 1s steps(20) both infinite; } @keyframes frame { 0% {background-position: 0 0;}//可省略 100% {background-position: -6000px 0;} } 複製程式碼
這裡我們定義了關鍵幀的開始和結束,也就是定義了一個關鍵幀週期,但因為我們沒有詳細的定義每一幀的展示,所以我們要將0%~100%這個區間分成20步來階段性展示。
也可以換成關鍵字的寫法,還可以只定義最後一幀,因為預設第一幀就是初始位置。
@keyframes frame { from {background-position: 0 0;}//可省略 to {background-position: -6000px 0;} } 複製程式碼
(3)連續移動雪碧圖位置(移動端推薦)
跟第二種基本一致,只是切換雪碧圖的位置過程換成了 transform:translate3d()
來實現,不過要加多一層 overflow: hidden;
的容器包裹,這裡我們以只定義初始和結束幀為例,使用 transform 可以開啟GPU加速,提高機器渲染效果,還能有效解決移動端幀動畫抖動的問題。
<div class="sprite-wp"> <div class="sprite"></div> </div> .sprite-wp { width: 300px; height: 300px; overflow: hidden; } .sprite { width: 6000px; height: 300px; will-change: transform; background: url(frame.png) no-repeat center; animation: frame 1s steps(20) both infinite; } @keyframes frame { 0% {transform: translate3d(0,0,0);} 100% {transform: translate3d(-6000px,0,0);} } 複製程式碼
三、JS幀動畫
(1)通過JS來控制img的src屬性切換(不推薦)
和上面CSS3幀動畫裡面切換元素 background-image
屬性一樣,會存在多個請求等問題,所以該方案我們不推薦,但是這是一種解決思路。
(2)通過JS來控制Canvas影象繪製
通過Canvas製作幀動畫的原理是用drawImage方法將圖片繪製到Canvas上,不斷擦除和重繪就能得到我們想要的效果。
<canvas id="canvas" width="300" height="300"></canvas> (function () { var timer = null, canvas = document.getElementById("canvas"), context = canvas.getContext('2d'), img = new Image(), width = 300, height = 300, k = 20, i = 0; img.src = "frame.png"; function drawImg() { context.clearRect(0, 0, width, height); i++; if (i == k) { i = 0; } context.drawImage(img, i * width, 0, width, height, 0, 0, width, height); } img.onload = function () { timer = setInterval(drawImg, 50); } })(); 複製程式碼
上面是通過改變裁剪影象的X座標位置來實現動畫效果的,也可以通過改變畫布上放置影象的座標位置實現,如下: context.drawImage(img, 0, 0, width*k, height,-i*width,0,width*k,height);
。
(3)通過JS來控制CSS屬性值變化
這種方式和前面CSS3幀動畫一樣,有三種方式,一種是通過JS切換元素背景圖片地址 background-image
,一種是通過JS切換元素背景圖片定位 background-position
,最後一種是通過JS移動元素 transform:translate3d()
,第一種不做介紹,因為同樣會存在多個請求等問題,不推薦使用,這裡實現後面兩種。
- 切換元素背景圖片位置
background-position
.sprite { width: 300px; height: 300px; background: url(frame.png) no-repeat 0 0; } <div class="sprite" id="sprite"></div> (function(){ var sprite = document.getElementById("sprite"), picWidth = 300, k = 20, i = 0, timer = null; // 重置背景圖片位置 sprite.style = "background-position: 0 0"; // 改變背景圖位置 function changePosition(){ sprite.style = "background-position: "+(-picWidth*i)+"px 0"; i++; if(i == k){ i = 0; } } timer = setInterval(changePosition, 50); })(); 複製程式碼
- 移動元素背景圖片位置
transform:translate3d()
.sprite-wp { width: 300px; height: 300px; overflow: hidden; } .sprite { width: 6000px; height: 300px; will-change: transform; background: url(frame.png) no-repeat center; } <div class="sprite-wp"> <div class="sprite" id="sprite"></div> </div> (function () { var sprite = document.getElementById("sprite"), picWidth = 300, k = 20, i = 0, timer = null; // 重置背景圖片位置 sprite.style = "transform: translate3d(0,0,0)"; // 改變背景圖移動 function changePosition() { sprite.style = "transform: translate3d(" + (-picWidth * i) + "px,0,0)"; i++; if (i == k) { i = 0; } } timer = setInterval(changePosition, 50); })(); 複製程式碼
為了便於理解,請大家閱讀以下內容,本節內容來自 Jovey 同學釋出的 《實用的 CSS — 動畫效能對比》 。
主執行緒和排版執行緒
在現代瀏覽器中,渲染頁面所要負責的執行緒主要有兩個:主執行緒和排版執行緒。
主執行緒
- 執行 JS
- 計算 HTML 元素的 CSS 樣式
- 佈局頁面
- 把頁面元素繪製成一個或多個位圖
- 把這些點陣圖移交給排版執行緒
在瀏覽器開始渲染頁面,或者長時間執行某個 JS時,主執行緒會一直在忙碌狀態,此時對於使用者的任何輸入或是操作都不會有所響應。
排版執行緒
- 通過 GPU 渲染點陣圖,並顯示在螢幕上
- 向主執行緒請求更新點陣圖的可見部分或即將可見的部分
- 判斷出當前頁面處於可見的部分
- 判斷出即將通過頁面滾動而可見的部分
- 隨著使用者滾動頁面來移動這些部分
排版執行緒對於使用者的操作保持快速的響應,普遍的幀率是每秒 60 幀的速度去渲染重新整理,顯示器是會以一定的頻率來重新整理顯示器,頻率是赫茲(Hz)。
Transtion
下面我們在網頁中實現一個元素的高度變化的動畫,滑鼠懸浮在元素上動畫啟動,直至完成,我們來了解一下瀏覽器的兩個執行緒是如何協同工作的:
<style> #foo { height: 100px; width: 100px; background: red; transition: height 1s linear; } #foo:hover { height: 200px; } </style> 複製程式碼
圖中橘黃色部分代表操作相對較慢,消耗較大;藍色部分代表操作相對較快,消耗較小

從上圖我們可以看到,瀏覽器的兩個執行緒在來回地切換工作,而且橘黃色出現次數較多,這意味著瀏覽器需要處理相當多的工作。
對於瀏覽器而言,由於元素的高度一直在變化,因此這個動畫的每一幀中,都需要重新佈局 ——> 繪製頁面 ——> 將新的點陣圖載入到 GPU 中 ——> 顯示。而其中載入到 GPU 是一個相對緩慢的操作。
Transform
經過上面的實驗,我們對 transition
屬性有了比較好的瞭解;同時我們對上述動畫效能也有一個瞭解。接著我需要在網頁中實現一個元素的大小變化動畫,滑鼠懸浮在元素上動畫啟動,直至完成:
<style> #bar { height: 100px; width: 100px; background: red; transition: transform 1s linear; } #bar:hover { transform: scale(2); } </style> 複製程式碼

由此我們可以看到,兩個執行緒來回切換的情況並不多,橘黃色部分出現的次數也較少,藍色部分居絕大部分,這意味著這個動畫效果相較於上面的要流暢很多。
在定義中, transform
是不會使瀏覽器產生重新排版的,因此 transform
不會影響原本的佈局,以及周圍的元素。它會將定義的元素作為一個整體進行縮放、移動或旋轉等。
基於 transform
這類的特性,瀏覽器在渲染頁面時可以節省很多不必要的開支,例如重新佈局和將點陣圖傳給 GPU 等工作,這樣就使得動畫更有效率。
所以,我們在選擇動畫方式時,應該優先選擇 transform
的實現方案。
方案總結
總結以上幾種方案,我們可以看到GIF圖有一定的優點也有缺點,所以這種方式是看情況選擇使用的,選擇符合實際場景的方案就是最好的方案。
同時我們最常用的是CSS3幀動畫,因為通過CSS就可以實現,效果也很好;如果希望新增更靈活豐富的互動就可以採用JS幀動畫的解決方案了。
通過上節的擴充套件閱讀,我們瞭解到, transform
的實現方案,在渲染效能上要優於 background-position
的實現方案,那其他實現方式效能如何呢,我們來比較一下。
測試環境:
系統:Windows 10 專業版 處理器:Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz 3.41GHz RAM: 8.00GB 瀏覽器:Chrome 67.0 複製程式碼
如果測試結果出現偏差,可能與測試環境變化有關。


這兩個FPS meter截圖是來自CSS3幀動畫,可以看到他們都能達到60FPS的流暢動畫效果,同時後者是改變背景圖位移 transform:translate3d()
,具有GPU加速效果。



這三個FPS meter截圖是來自JS幀動畫,分別是Canvas繪製,改變 background-position
,改變 transform:translate3d()
,可以看到JS幀動畫的FPS都只有20左右,這個數值的FPS會給人感覺一定的卡頓和不舒適感,同時也看到,他們的波線或多或少有一定的不穩定性,這同樣會給人卡頓的感覺,而且不難看出,使用了transform 3D屬性具有GPU加速效果,在裝置上表現相對會好一點。
綜上我們可以對各方案動畫效能簡單排序:
GPU 硬體加速CSS3動畫 > 非硬體加速CSS3動畫 > GPU 硬體加速Javascript 動畫 > 非硬體加速Javascript 動畫
tips:使用 will-change 可以在元素屬性真正發生變化之前提前做好對應準備
注意事項
素材:動畫圖片寬高最好是偶數,總幀數最好是偶數,圖片拼接處最好有一定的留白。
適配:移動端適配最好不用rem,因為rem的計算會造成小數四捨五入,造成一定的抖動效果,建議直接用px作為單位,同時輔助以scale(zoom)媒體查詢進行適配。如果使用rem適配,試試使用 transform 的方案,抖動問題可以得到優化解決。
對於幀與幀之間的 盈虧互補 現象導致動畫抖動,想要了解更多,可以閱讀 《CSS技巧:逐幀動畫抖動解決方案》 。