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

Tom Wiltzius
Tom Wiltzius

簡介

您希望網頁應用程式在執行動畫、轉場效果和其他小型 UI 效果時,獲得快速回應和流暢的體驗。要確保這些效果沒有卡頓,可能代表「原生」或排擠等畫面較雜亂的情形。

本系列文章為一系列文章, 詳細介紹如何在瀏覽器中呈現轉譯效能最佳化。首先,我們會說明流暢的動畫較難的原因、需要完成哪些動作,以及一些簡單的最佳做法。這些構想大多都是透過「Jank Busters」來構思,講稿和今年的 Google I/O 大會講座 (影片)。

隆重推出 V-sync

電腦遊戲玩家可能會熟悉這個詞,但在網路上很罕見:什麼是 v-sync

考量手機螢幕:會定期重新整理螢幕,但通常 (但並非一定) 會每秒更新 60 次。V 同步 (或垂直同步處理) 是指僅在畫面重新整理之間產生新影格的做法。您可能會認為這就像是程序之間的競爭狀況,也就是將資料寫入螢幕緩衝區的程序,與作業系統讀取資料並放到螢幕上的作業系統之間。我們希望緩衝影格內容在重新整理兩次 (而非執行期間) 之間變更,否則監視器只會顯示一幅影格的一半,並導致「撕裂」。

如要拍出流暢的動畫,每次畫面重新整理時,都必須新增一個影格都要準備就緒。這有兩個重大的影響:影格時間 (也就是影格準備工作時) 和影格預算 (也就是瀏覽器產生影格的時間長度)。畫面重新整理的間隔時間只有完成影格一次 (在 60Hz 螢幕上約 16 毫秒),且您希望在畫面顯示最後一個影格後立即產生下一個影格。

時間是一切:requestAnimationFrame

許多網頁程式開發人員每 16 毫秒會使用 setIntervalsetTimeout 來建立動畫。造成這個問題的原因很多 (我們會在 1 分鐘內進一步討論),但具體問題如下:

  • JavaScript 的計時器解析度只能依數毫秒順序
  • 不同裝置的刷新率也不同

請回想上述影格時間問題:您必須在下一個畫面重新整理之前,備妥完整的動畫影格,以及所有 JavaScript、DOM 操控、版面配置、繪製等程序。如果計時器解析度偏低,可能會導致動畫影格在下一個畫面重新整理前就難以辨識,但畫面刷新率的變化則讓固定計時器無法完整呈現。無論計時器的間隔時間為何,您都會慢慢從影格計時開始,最後再捨棄計時器。即使以毫秒準確度觸發計時器,也不會以毫秒為單位觸發 (因為開發人員「發現」不同),計時器解析度也會受到背景分頁佔用資源等因素影響。即使這種情況相當罕見 (例如每 16 個畫面因一毫秒影格中斷而下降),您就會收到通知:您也會著手產生未顯示的影格,浪費電力和 CPU 作業時間,避免應用程式浪費在應用程式執行其他作業。

不同的螢幕有不同的刷新率:60 Hz 通常很常見,但部分手機的刷新率為 59 Hz,某些筆記型電腦在低耗電模式下降至 50Hz,而部分桌上型電腦的刷新率則為 70 Hz。

談論轉譯效能時,我們往往著重於每秒影格數 (FPS),但差異可能會更嚴重。我們的眼睛發現動畫中細微的不規則命中,就是時間不規律產生的動畫。

取得正確計時動畫影格的方法是使用 requestAnimationFrame。使用這個 API 時,您將會要求瀏覽器建立動畫頁框。當瀏覽器即將產生新頁框時,就會呼叫回呼。無論重新整理頻率為何,都會發生這種情形。

requestAnimationFrame 也具備其他實用屬性:

  • 系統會暫停背景分頁中的動畫,節省系統資源和電池壽命。
  • 如果系統無法以螢幕刷新率處理算繪作業,就會限制動畫,並減少回呼的頻率 (例如在 60Hz 螢幕上每秒 30 次)。這會將影格速率降為一半,但動畫效果保持一致。如上所述,遠比影格速率的偏差更大。穩定的 30 Hz 比 60 Hz 來得好,而且每秒錯過幾影格數。

requestAnimationFrame 已全程討論過,因此如需更多相關資訊,請參閱廣告素材 JS 中的文章,但這是流暢動畫的重要步驟。

畫面預算

由於我們希望在每次畫面重新整理時都為新的頁框,因此每次重新整理後都只需要執行所有工作建立新的頁框。在 60Hz 螢幕上,我們必須大約 16 毫秒才能執行所有 JavaScript、執行版面配置、繪製,以及瀏覽器為了使頁框傳出的任何作業。換句話說,如果 requestAnimationFrame 回呼中的 JavaScript 需要超過 16 毫秒的執行時間,您就不會有希望在 v-sync 上加速產生影格!

16 毫秒不會太久。幸好,如果您因為 requestAnimationFrame 回呼時用完了影格預算,Chrome 的開發人員工具可協助您追蹤影格預算。

開啟開發人員工具時間軸,快速錄製這段動畫的實際運作情況,即可看出動畫播放期間會超出預算。然後在時間軸中切換至「影格」,然後一探究竟:

版面配置過多的示範
版面配置過多的示範

這些 requestAnimationFrame (rAF) 回呼花費超過 200 毫秒。這個規模的時間太長,不能每 16 毫秒就注意到一個影格!開啟其中一個較長的 rAF 回呼,即可瞭解內部動態:在本例中,會有許多版面配置。

保羅的影片詳細說明瞭重新版面配置的具體原因 (也就是讀取 scrollTop),以及如何避免這種情況。但重點是,您可以深入瞭解回呼,並調查耗時多久。

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

請注意 16 毫秒的影格時間。頁框中的空白空間就是您的進步空間,也就是您需要執行更多工作 (或讓瀏覽器在背景執行需要的工作)。那上面的空白區域是個好東西。

其他 Jank 來源

嘗試執行 JavaScript 動畫時,問題的最大原因是,其他內容可能會妨礙 rAF 回呼,甚至完全無法執行。即使 rAF 回呼保持精簡且只在幾毫秒內執行,其他活動 (例如處理剛傳入的 XHR、執行輸入事件處理常式,或對計時器執行排程更新) 可能會突然發生,而且不會產生任何時間。在行動裝置上,處理這類事件有時可能需要數百毫秒,因為動畫會完全停滯。我們將這些動畫「卡頓」稱為「卡頓」。

沒有任何魔法可以避免這類狀況,不過只要參考以下幾個架構最佳做法,就能奠定成功基礎:

  • 請勿在輸入處理常式中執行大量處理作業!如果在過程中執行大量 JS 或嘗試重新排列整個網頁,例如捲動式處理常式是很容易造成資源浪費的常見原因。
  • 請盡可能將更多處理程序 (讀取:會花費較長時間的項目) 推送至 rAF 回呼或 Web Worker
  • 若將工作推入 rAF 回呼,請試著將其分段,以便只處理每個影格,或是延遲到重要動畫結束後再執行,這樣您就能持續執行簡短的 rAF 回呼,讓動畫流暢地進行。

如需說明如何將處理程序推送至 requestAnimationFrame 回呼 (而非輸入處理常式) 的優質教學課程,請參閱 Paul Lewis 的「Leaner、Meaner、Faster Animations with requestAnimationFrame」。

CSS 動畫

在事件和 rAF 回呼中,比起輕量的 JS 有哪些好處?不支援 JS。

先前我們曾表示,為了避免中斷 rAF 回呼,雖然沒有效果上的銀色,但您可以使用 CSS 動畫來完全不需要。尤其是在 Chrome for Android 上 (其他瀏覽器類似的功能) 中,CSS 動畫具有非常理想的屬性,即使 JavaScript 執行中,瀏覽器也通常能夠執行這些動畫。

上述章節中關於卡頓的隱含陳述式:瀏覽器一次只能執行一項操作。這並非確實如此,但建議您假設瀏覽器在任何特定時間都可以執行 JS、執行版面配置或繪畫,但一次只能執行一項。請前往開發人員工具的時間軸檢視畫面確認這項操作。這項規則的例外是 Android 版 Google 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 動畫驅動該動畫,就不會再發生卡頓。

(請注意,在本文撰寫期間,在 Android 版 Google Chrome 中,CSS 動畫不會發生卡頓,電腦版的 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 動畫,甚至是捲動靜態網頁。本系列的下一篇文章將深入探討這些概念,並著重在捲動式成效。

祝你動畫愉快!

參考資料