資源浪費,以提升轉譯效能

Tom Wiltzius
Tom Wiltzius

簡介

您希望網頁應用程式在執行動畫、轉場和其他小型 UI 效果時,能提供流暢的回應體驗。確保這些效果不會出現卡頓現象,才能營造「原生」的感受,而不是笨拙、不夠精緻的感受。

這是一系列文章的第一篇,將說明如何在瀏覽器中最佳化轉譯效能。首先,我們將說明為何要製作流暢的動畫、如何製作流暢的動畫,以及一些簡單的最佳做法。這些想法最初是在「Jank Busters」中提出,這是今年 Google I/O 大會上 Nat Duca 和我發表的演講 (影片)。

隆重推出 V-sync

v-sync 是 PC 遊戲玩家熟悉的術語,但在網路上並不常見。

以手機螢幕為例,螢幕會以固定間隔進行重新整理,通常 (但不一定) 每秒約 60 次。V-sync (或垂直同步) 是指只在畫面重新整理期間產生新影格的做法。您可能會認為,這就像是將資料寫入螢幕緩衝區的程序與作業系統讀取資料並將其顯示在螢幕上的競爭狀態。我們希望緩衝影格內容在這些重新整理期間之間變更,而不是在重新整理期間內變更;否則,監視器會顯示一半的影格和另一半的影格,導致「撕裂」現象。

為了呈現流暢的動畫,您需要在每次螢幕重新整理時準備新的影格。這有兩個重大影響:影格時間 (即影格需要準備就緒的時間) 和影格預算 (即瀏覽器產生影格所需的時間)。您只有在螢幕重新整理之間的時間來完成影格 (60Hz 螢幕約為 16 毫秒),而且您希望在上一影格顯示在螢幕上後,立即開始產生下一影格。

時機就是一切:requestAnimationFrame

許多網頁開發人員會每隔 16 毫秒使用 setIntervalsetTimeout 來製作動畫。這會造成許多問題 (我們稍後會進一步討論),但特別值得注意的是:

  • JavaScript 的計時器解析度僅為數毫秒的順序
  • 不同裝置的螢幕重新整理頻率不同

回想上述提到的時間軸問題:您需要完成動畫影格,並完成任何 JavaScript、DOM 操作、版面配置、繪圖等作業,才能在下次螢幕重新整理前準備就緒。計時器解析度過低,可能會導致動畫影格無法在下次螢幕刷新前完成,但螢幕刷新率的變化會導致固定計時器無法執行。無論計時器間隔為何,您都會逐漸超出影格計時視窗,最後會捨棄一個影格。即使計時器以毫秒精確度啟動,也會發生這種情況 (但開發人員發現,計時器不會以毫秒精確度啟動)。計時器解析度會因裝置是使用電池還是插電而異,也可能受到背景分頁佔用資源等因素影響。即使這種情況很少發生 (例如每 16 格就會發生一次,因為您誤差了 1 毫秒),您還是會發現每秒會遺失好幾格。您也必須產生永遠不會顯示的影格,這會浪費電力和 CPU 時間,而您本可將這些時間用於在應用程式中執行其他作業。

不同螢幕的刷新率不同:60 Hz 是最常見的刷新率,但部分手機的刷新率為 59 Hz,部分筆電在省電模式下會降至 50 Hz,部分電腦螢幕的刷新率則為 70 Hz。

討論算繪效能時,我們通常會著重於每秒影格數 (FPS),但變化幅度可能會是更大的問題。我們發現動畫中出現不規則的微小卡頓,這可能是時間不準確的動畫所造成。

如要取得正確時間的動畫影格,請使用 requestAnimationFrame。使用這個 API 時,您會要求瀏覽器提供動畫影格。當瀏覽器即將產生新影格時,系統會呼叫您的回呼。無論重新整理頻率為何,都會發生這種情況。

requestAnimationFrame 也有其他不錯的屬性:

  • 背景分頁中的動畫會暫停,以節省系統資源和電池續航力。
  • 如果系統無法以螢幕的刷新率處理轉譯作業,則可能會降低動畫速度,並減少回呼產生頻率 (例如,在 60 Hz 螢幕上每秒 30 次)。雖然這會使影格速率降低一半,但仍可維持動畫的一致性。如上所述,我們的眼睛比起影格速率,更能適應變化。穩定的 30 Hz 比每秒少幾格影格的 60 Hz 更理想。

requestAnimationFrame 已在各處討論過,因此請參閱 這篇 Creative JS 文章,進一步瞭解相關資訊。不過,這是打造流暢動畫的重要第一步。

影格預算

由於我們希望每次螢幕重新整理時都能準備好新影格,因此只有在重新整理之間的時間內,才能執行所有建立新影格的作業。在 60Hz 螢幕上,這表示我們有大約 16 毫秒的時間來執行所有 JavaScript、執行版面配置、繪製畫面,以及瀏覽器為了產生影格而必須執行的其他作業。也就是說,如果 requestAnimationFrame 回呼中的 JavaScript 執行時間超過 16 毫秒,您就無法在 v-sync 的時間內產生影格!

16 毫秒並不是很長的時間。幸好,如果您在 requestAnimationFrame 回呼期間超出影格預算,Chrome 的開發人員工具可以協助您追蹤問題。

開啟「Dev Tools」時間軸,並記錄動畫運作情形,很快就能發現動畫製作時超出預算。在時間軸中切換至「Frames」,並查看以下內容:

含有過多版面的範例
含有過多版面的示範畫面

這些 requestAnimationFrame (rAF) 回呼需要超過 200 毫秒。每 16 毫秒產生一個影格,這麼長的時間太久了!開啟其中一個長時間的 rAF 回呼,即可瞭解內部發生了什麼事:在本例中,有許多版面配置。

Paul 的影片會進一步說明重新配置的具體原因 (讀取 scrollTop) 以及如何避免這種情況。但重點是,您可以深入瞭解回呼,並調查造成延遲的原因。

更新後的示範內容,版面配置大幅減少
更新後的示範內容,版面配置大幅縮減

請注意 16 毫秒的幀時間。在影格中留白的空間就是您需要執行更多工作 (或讓瀏覽器在背景執行所需的工作) 的空間。空白空間是好事。

其他卡頓來源

嘗試執行以 JavaScript 為動畫動力的動畫時,最常見的問題是其他內容會干擾 rAF 回呼,甚至完全阻止回呼執行。即使 rAF 回呼很精簡,且只在幾毫秒內執行,其他活動 (例如處理剛收到的 XHR、執行輸入事件處理常式,或在計時器上執行預定更新) 仍可能突然出現,並在任何時間點執行,而不會產生產出。在行動裝置上,處理這些事件有時可能需要數百毫秒,這段時間內動畫會完全停頓。我們將這些動畫卡頓現象稱為「jank」

雖然沒有萬靈丹可避免這些情況,但您可以採取一些架構最佳做法,奠定成功基礎:

  • 請勿在輸入處理常式中執行大量處理作業!執行大量 JS 或嘗試在 onscroll 處理常式期間重新排列整個網頁,是造成嚴重卡頓現象的常見原因。
  • 盡可能將大部分的處理作業 (也就是任何需要長時間執行的作業) 推送至 rAF 回呼或 Web Workers
  • 如果您將工作推送至 rAF 回呼,請嘗試將工作分割成多個部分,以便在每個影格中只處理一小部分工作,或延遲至重要動畫結束後再處理,這樣一來,您就能繼續執行短暫的 rAF 回呼,並流暢地執行動畫。

如需教學課程,瞭解如何將處理作業推送至 requestAnimationFrame 回呼 (而非輸入處理常式),請參閱 Paul Lewis 的文章「使用 requestAnimationFrame 製作更精簡、更強大、更快速的動畫」。

CSS 動畫

在事件和 rAF 回呼中,有哪些比輕量 JS 更適合?不使用 JS。

我們先前提到,沒有萬靈丹可避免中斷 rAF 回呼,但您可以使用 CSS 動畫,完全不必使用回呼。特別是在 Android 版 Chrome (以及其他瀏覽器正在開發的類似功能) 中,CSS 動畫具有非常實用的特性,即使 JavaScript 正在執行,瀏覽器也能經常執行動畫。

上述有關 jank 的部分有一個隱含的陳述式:瀏覽器一次只能執行一項操作。這並非絕對正確,但仍是可行的假設:瀏覽器在任何時間點都能執行 JS、執行版面配置或繪圖,但一次只能執行其中一種作業。您可以在「開發人員工具」的「時間軸」檢視畫面中確認這項資訊。這項規則的例外狀況之一,是 Android 版 Chrome 上的 CSS 動畫 (以及即將推出的 Chrome 桌面版)。

盡可能使用 CSS 動畫,這樣不僅可簡化應用程式,還能讓動畫順暢執行,即使在執行 JavaScript 時也不例外。

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

如果您按下按鈕,JavaScript 會執行 180 毫秒,導致卡頓。但如果我們改用 CSS 動畫來驅動動畫,就不會發生卡頓現象。

(請注意,在撰寫本文時,CSS 動畫只有在 Android 版 Chrome 上才不會出現卡頓現象,電腦版 Chrome 則不會)。

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

如要進一步瞭解如何使用 CSS 動畫,請參閱 MDN 上的這篇文章

總結

簡單來說:

  1. 製作動畫時,為每個螢幕刷新作業產生影格非常重要。Vsync 動畫對應用程式的使用體驗有極大正面影響。
  2. 在 Chrome 和其他新式瀏覽器中取得 vsync 動畫的最佳方法,就是使用 CSS 動畫。如果您需要比 CSS 動畫提供的更大的彈性,最佳做法是使用以 requestAnimationFrame 為基礎的動畫。
  3. 為確保 rAF 動畫正常運作,請確認其他事件處理常式不會妨礙 rAF 回呼的執行,並確保 rAF 回呼的執行時間不長 (小於 15 毫秒)。

最後,vsync 動畫不僅適用於簡單的 UI 動畫,也適用於 Canvas2D 動畫、WebGL 動畫,甚至是靜態網頁的捲動畫面。在本系列的下一篇文章中,我們將深入探討捲動效能,並納入這些概念。

祝您動畫製作愉快!

參考資料