大幅提升網頁應用程式的動畫效能
簡而言之:您可以使用 Animation Worklet 編寫命令式動畫,以裝置的原生影格速率執行,讓動畫更加流暢,不會出現卡頓情形。此外,這類動畫也更不容易受到主要執行緒卡頓的影響,而且可以連結至捲動,而非時間。動畫 Worklet 位於 Chrome Canary 版 (「Experimental Web Platform features」旗標後方),我們預計在 Chrome 71 版中進行來源試用。您今天就能開始使用,逐步改善體驗。
其他動畫 API?
其實不是,這是我們現有功能的延伸,而且有充分的理由! 讓我們從頭開始。如要在網頁上為任何 DOM 元素製作動畫,目前有 2 ½ 種選擇:CSS 轉場效果,適用於簡單的 A 到 B 轉場效果;CSS 動畫,適用於可能循環的複雜時間軸動畫;Web Animations API (WAAPI),適用於幾乎任意複雜的動畫。WAAPI 的支援矩陣看起來相當不樂觀,但情況正在好轉。在此之前,請使用 polyfill。
這些方法的共通點是無狀態且以時間為準。但開發人員嘗試的部分效果既非時間驅動,也非無狀態。舉例來說,惡名昭彰的視差捲動器顧名思義就是由捲動驅動。令人意外的是,如今要在網路上導入高效能的視差捲動器相當困難。
無狀態呢?以 Android 上的 Chrome 網址列為例,如果向下捲動,橫幅就會從畫面上消失。但只要向上捲動,即使只捲動到頁面一半,工具列也會重新出現。動畫不僅取決於捲動位置,也取決於先前的捲動方向。這是有狀態的。
另一個問題是捲軸樣式。眾所皆知,這些元素無法設定樣式,或至少無法充分設定樣式。如果我想要彩虹貓做為捲軸,該怎麼辦?無論選擇哪種技術,建立自訂捲軸都不容易,而且效能也不佳。
重點是,這些做法都很不方便,而且很難有效率地實作,甚至不可能。其中大多數都依賴事件和/或 requestAnimationFrame
,即使螢幕能夠以 90fps、120fps 以上的影格速率執行,且只佔用一小部分珍貴的主執行緒影格預算,仍可能將影格速率限制在 60fps。
動畫 Worklet 擴充了網頁動畫堆疊的功能,可讓這類效果更容易實現。開始之前,請先確認我們已瞭解動畫的基本概念。
動畫和時間軸基本概念
WAAPI 和 Animation Worklet 會大量使用時間軸,讓您以想要的方式編排動畫和效果。本節將快速複習或介紹時間軸,以及時間軸如何與動畫搭配運作。
每份文件都有 document.timeline
。文件建立時會從 0 開始,並計算文件存在時間的毫秒數。文件中的所有動畫都會根據這個時間軸運作。
為了讓您更清楚瞭解,我們來看看這段 WAAPI 程式碼片段
const animation = new Animation(
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
{
transform: 'translateY(500px)',
},
],
{
delay: 3000,
duration: 2000,
iterations: 3,
}
),
document.timeline
);
animation.play();
呼叫 animation.play()
時,動畫會使用時間軸的 currentTime
做為開始時間。我們的動畫延遲時間為 3000 毫秒,也就是說,當時間軸到達 `startTime` 時,動畫就會開始 (或「啟用」)。
- 3000
. After that time, the animation engine will animate the given element from the first keyframe (
translateX(0)), through all intermediate keyframes (
translateX(500px)) all the way to the last keyframe (
translateY(500px)) in exactly 2000ms, as prescribed by the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000`。重點是,時間軸會控制動畫的進度!
動畫到達最後一個主要畫面格後,會跳回第一個主要畫面格,並開始下一次動畫疊代。由於我們設定了 iterations: 3
,這個程序總共會重複 3 次。如要讓動畫永不停止,請寫入 iterations: Number.POSITIVE_INFINITY
。以下是上述程式碼的結果。
WAAPI 功能強大,還有許多其他功能,例如緩和效果、起始偏移、關鍵影格權重和填滿行為,這些都超出本文範圍。如要瞭解詳情,建議閱讀 這篇 CSS Tricks 上的 CSS 動畫文章。
撰寫動畫 Worklet
現在我們已瞭解時間軸的概念,可以開始瞭解 Animation Worklet,以及如何使用這項功能調整時間軸!Animation Worklet API 不僅以 WAAPI 為基礎,就可擴充網頁而言,也是說明 WAAPI 函式運作方式的低層級基本元素。兩者的語法非常相似:
動畫小程式 | WAAPI |
---|---|
new WorkletAnimation( 'passthrough', new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
new Animation( new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
兩者的差異在於第一個參數,也就是驅動這項動畫的 worklet 名稱。
特徵偵測
Chrome 是第一個推出這項功能的瀏覽器,因此請務必確保程式碼不會只預期有 AnimationWorklet
。因此,在載入 Worklet 之前,我們應先偵測使用者的瀏覽器是否支援 AnimationWorklet
,方法很簡單:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
載入 Worklet
工作單是 Houdini 工作小組推出的新概念,可簡化許多新 API 的建構和擴充作業。我們稍後會詳細說明工作單,但為求簡單,您目前可以將工作單視為便宜且輕量的執行緒 (類似於工作站)。
我們需要確保已載入名為「passthrough」的 worklet,再宣告動畫:
// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...
// passthrough-aw.js
registerAnimator(
'passthrough',
class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
}
);
這是怎麼回事?我們使用 AnimationWorklet 的 registerAnimator()
呼叫,將類別註冊為動畫師,並將其命名為「passthrough」。這與我們在上述 WorkletAnimation()
建構函式中使用的名稱相同。註冊完成後,addModule()
傳回的 Promise 會解析,我們就能開始使用該工作單建立動畫。
系統會為瀏覽器要算繪的每個影格呼叫例項的 animate()
方法,並傳遞動畫時間軸的 currentTime
,以及目前正在處理的特效。我們只有一個效果 (KeyframeEffect
),並使用 currentTime
設定效果的 localTime
,因此這個動畫師稱為「passthrough」。有了這個工作單的程式碼,WAAPI 和 AnimationWorklet 的行為完全相同,如示範所示。
時間
animate()
方法的 currentTime
參數是時間軸的 currentTime
,我們已將時間軸傳遞至 WorkletAnimation()
建構函式。在前一個範例中,我們只是將時間傳遞至效果。但由於這是 JavaScript 程式碼,我們可以扭曲時間 💫
function remap(minIn, maxIn, minOut, maxOut, v) {
return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
'sin',
class {
animate(currentTime, effect) {
effect.localTime = remap(
-1,
1,
0,
2000,
Math.sin((currentTime * 2 * Math.PI) / 2000)
);
}
}
);
我們會取得 Math.sin()
的 currentTime
,然後將該值重新對應至 [0; 2000] 範圍,這是我們定義效果的時間範圍。現在動畫看起來很不一樣,但我們沒有變更主要畫面格或動畫選項。小程式碼可任意複雜,並允許您以程式輔助方式定義要播放哪些效果、播放順序和程度。
選項優先於選項
您可能想重複使用工作站,並變更其號碼。因此,您可以使用 WorkletAnimation 建構函式,將選項物件傳遞至工作單:
registerAnimator(
'factor',
class {
constructor(options = {}) {
this.factor = options.factor || 1;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.factor;
}
}
);
new WorkletAnimation(
'factor',
new KeyframeEffect(
document.querySelector('#b'),
[
/* ... same keyframes as before ... */
],
{
duration: 2000,
iterations: Number.POSITIVE_INFINITY,
}
),
document.timeline,
{factor: 0.5}
).play();
在這個範例中,兩個動畫都是以相同程式碼驅動,但選項不同。
請告訴我你所在的州/省!
如先前所述,動畫工作單要解決的主要問題之一,就是有狀態的動畫。動畫工作單元可保留狀態。不過,工作單的核心功能之一是可遷移至其他執行緒,甚至可銷毀以節省資源,這也會銷毀工作單的狀態。為避免狀態遺失,動畫工作單元提供一個在工作單元遭到刪除「之前」呼叫的 Hook,可用於傳回狀態物件。重新建立工作單時,系統會將該物件傳遞至建構函式。首次建立時,該參數會是 undefined
。
registerAnimator(
'randomspin',
class {
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
// Some math to make sure that `localTime` is always > 0.
effect.localTime = 2000 + this.direction * (currentTime % 2000);
}
destroy() {
return {
direction: this.direction,
};
}
}
);
每次重新整理這個範例,正方形都有 50% 的機率會朝某個方向旋轉。如果瀏覽器要拆除工作單元並遷移至其他執行緒,建立時會再次呼叫 Math.random()
,這可能會導致方向突然改變。為確保不會發生這種情況,我們會以 state 形式傳回隨機選擇方向的動畫,並在建構函式中使用 (如有提供)。
連結時空連續體:ScrollTimeline
如上一節所示,AnimationWorklet 可讓我們以程式輔助方式定義時間軸前進時,對動畫效果的影響。但目前為止,我們的時間軸一律是 document.timeline
,可追蹤時間。
ScrollTimeline
開啟了新的可能性,讓您可以使用捲動而非時間來驅動動畫。我們將重複使用第一個「直通」工作單,進行這項示範:
new WorkletAnimation(
'passthrough',
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
],
{
duration: 2000,
fill: 'both',
}
),
new ScrollTimeline({
scrollSource: document.querySelector('main'),
orientation: 'vertical', // "horizontal" or "vertical".
timeRange: 2000,
})
).play();
我們建立新的 ScrollTimeline
,而不是傳遞 document.timeline
。
您可能已經猜到,ScrollTimeline
不會使用時間,而是使用 scrollSource
的捲動位置,在 worklet 中設定 currentTime
。捲動至最上方 (或最左側) 時,currentTime = 0
會設為 0,而捲動至最下方 (或最右側) 時,currentTime
則會設為 timeRange
。如果您捲動這個示範中的方塊,即可控制紅色方塊的位置。
如果您使用不會捲動的元素建立 ScrollTimeline
,時間軸的 currentTime
會是 NaN
。因此,請務必做好準備,將 NaN
視為 currentTime
,特別是考慮到回應式設計時。通常預設為 0 較為合理。
將動畫與捲動位置連結是長期以來的目標,但一直無法達到這個精確程度 (除了使用 CSS3D 的變通方法)。Animation Worklet 可讓您以簡單直接的方式實作這些效果,同時維持高效能。舉例來說,這個示範的視差捲動效果顯示,現在只要幾行程式碼,就能定義捲動驅動的動畫。
深入解析
Worklet
Worklet 是 JavaScript 環境,具有獨立範圍和極小的 API 介面。API 介面較小,因此瀏覽器可以進行更積極的效能最佳化,尤其是在低階裝置上。此外,工作單不會繫結至特定事件迴圈,但可視需要於執行緒之間移動。這對 AnimationWorklet 來說尤其重要。
Compositor NSync
您可能知道某些 CSS 屬性可以快速製作動畫,其他則不行。有些屬性只需要在 GPU 上進行一些作業即可產生動畫,有些則會強制瀏覽器重新排版整個文件。
在 Chrome 中 (以及許多其他瀏覽器),我們有一個稱為合成器的程序,其工作是排列圖層和紋理,然後盡可能規律地使用 GPU 更新畫面,最好是畫面更新速度越快越好 (通常為 60Hz)。視動畫的 CSS 屬性而定,瀏覽器可能只需要讓合成器執行作業,其他屬性則需要執行版面配置,而這項作業只能由主執行緒執行。視您打算製作動畫的屬性而定,動畫工作單會繫結至主執行緒,或在與合成器同步的獨立執行緒中執行。
輕微處罰
GPU 是競爭激烈的資源,因此通常只有一個合成器程序,可能會在多個分頁之間共用。如果合成器遭到封鎖,整個瀏覽器就會停止運作,無法回應使用者輸入內容。請務必避免這種情況。如果工作單元無法及時提供合成器所需的資料,導致影格無法順利算繪,會發生什麼情況?
如果發生這種情況,根據規格,工作單可以「滑動」。這會落後於合成器,而合成器可以重複使用上一個影格的資料,以維持影格速率。在視覺上,這看起來會像是卡頓,但最大的差異在於瀏覽器仍會回應使用者輸入。
結論
AnimationWorklet 有許多面向,可為網路帶來許多好處。 顯而易見的好處是,您可以進一步控制動畫,並以新方式驅動動畫,為網頁帶來全新層次的視覺保真度。但 API 設計也允許您讓應用程式更不容易發生延遲,同時存取所有新功能。
動畫 Worklet 目前位於 Canary 版,我們目標是在 Chrome 71 中進行初期試用。我們很期待你打造出優質的全新網站體驗,並分享需要改進的地方。此外,還有一個 polyfill 提供相同的 API,但不提供效能隔離功能。
請注意,CSS 轉場效果和 CSS 動畫仍是有效的選項,對於基本動畫來說,這些選項可能簡單許多。但如果需要更精緻的動畫,AnimationWorklet 就能派上用場!