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

Tom Wiltzius
Tom Wiltzius

簡介

您希望網頁應用程式在執行動畫、轉場和其他小型 UI 效果時,能提供流暢的回應。確保這些效果不會卡頓,就能讓使用者感受到「原生」和笨拙、未經修飾的差異。

本文是系列文章的第一篇,介紹如何最佳化瀏覽器的轉譯效能。首先,我們會說明為何難以實現流暢的動畫效果、需要採取哪些措施才能達成,以及一些簡單的最佳做法。其中許多構想最初是在「Jank Busters」中提出,這是 Nat Duca 和我在今年 Google I/O 大會上發表的演講 (影片)。

垂直同步簡介

電腦遊戲玩家可能熟悉這個詞,但在網路上並不常見:什麼是 v-sync?

以手機螢幕為例:螢幕會定期重新整理,通常 (但並非總是) 每秒約 60 次。垂直同步 (V-sync) 是指僅在畫面重新整理之間產生新影格的做法。這就像是將資料寫入螢幕緩衝區的程序,與作業系統讀取資料並顯示在螢幕上的程序之間,發生了競爭狀況。我們希望緩衝的影格內容在這些重新整理之間變更,而不是在重新整理期間變更,否則螢幕會顯示一半的影格和另一半的影格,導致「畫面撕裂」。

如要取得流暢的動畫,每次螢幕重新整理時,都必須準備好新的影格。這會產生兩大影響:影格時間 (即影格必須準備就緒的時間) 和影格預算 (即瀏覽器產生影格所需的時間)。您只有在螢幕重新整理之間的空檔,才能完成一個影格 (在 60Hz 螢幕上約 16 毫秒),而且您希望在最後一個影格顯示在螢幕上後,立即開始產生下一個影格。

時機就是一切:requestAnimationFrame

許多網頁程式開發人員會每 16 毫秒使用 setInterval 或 setTimeout 建立動畫。這會造成許多問題 (稍後會進一步討論),但特別值得關注的是:

  • JavaScript 的計時器解析度只有幾毫秒
  • 不同裝置的刷新率不同

回想一下上述的影格時間問題:您需要一個完整的動畫影格,並完成所有 JavaScript、DOM 操作、版面配置、繪製等,才能在下一次螢幕重新整理前準備就緒。計時器解析度過低,可能會導致動畫影格無法在下次螢幕刷新前完成,但螢幕刷新率的變化,會導致固定計時器無法完成。無論計時器間隔為何,您都會慢慢偏離影格的時間範圍,最後導致影格遺失。即使計時器以毫秒為單位觸發,也會發生這種情況,但計時器不會 (如開發人員發現) -- 計時器解析度會因機器是否使用電池或插電而異,可能會受到背景分頁佔用資源等影響。即使這種情況很少見 (例如每 16 個影格,因為您差了一毫秒),您也會注意到:每秒會遺失幾個影格。此外,您也將產生永遠不會顯示的影格,這會浪費電力和 CPU 作業時間,而這些時間原本可用於在應用程式中執行其他作業。

不同螢幕的刷新率也不同:60 Hz 很常見,但有些手機是 59 Hz,有些筆電在省電模式下會降到 50 Hz,有些電腦螢幕則是 70 Hz。

討論轉譯效能時,我們通常會著重於每秒影格數 (FPS),但變異數可能是更大的問題。如果動畫時間不當,我們的眼睛會注意到動畫中微小、不規則的停頓。

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

requestAnimationFrame 還有其他優點:

  • 背景分頁中的動畫會暫停,節省系統資源和電池電力。
  • 如果系統無法以螢幕的刷新率處理轉譯作業,可能會限制動畫,並降低回呼頻率 (例如在 60Hz 螢幕上每秒 30 次)。雖然這會使影格速率減半,但能保持動畫一致,而且如上所述,我們的眼睛對變化的敏感度遠高於影格速率。穩定的 30Hz 畫面,看起來比每秒遺漏幾個影格的 60Hz 畫面更流暢。

requestAnimationFrame 已在各處討論,因此請參閱 Creative JS 的這篇文章等文章,瞭解更多資訊,但這是流暢動畫的重要第一步。

影格預算

由於我們希望每次螢幕重新整理時都能準備好新影格,因此只有在重新整理之間的空檔,才能完成建立新影格的所有工作。在 60Hz 螢幕上,這表示我們有約 16 毫秒的時間執行所有 JavaScript、執行版面配置、繪製,以及瀏覽器必須執行的任何其他作業,才能取得影格。也就是說,如果 requestAnimationFrame 回呼中的 JavaScript 執行時間超過 16 毫秒,就無法及時產生影格,與垂直同步保持一致!

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

開啟開發人員工具時間軸,並錄製這個動畫的實際運作情形,很快就會發現動畫的預算遠遠超標。在時間軸中切換至「影格」並查看:

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

這些 requestAnimationFrame (rAF) 回呼需要超過 200 毫秒。這比每 16 毫秒產生一個影格的時間長了一個數量級!開啟其中一個冗長的 rAF 回呼,即可查看內部情況:在本例中,有許多版面配置。

Paul 的影片詳細說明瞭重新版面配置的具體原因 (讀取 scrollTop) 以及如何避免。但重點是,您可以深入查看回呼,並調查耗時過長的原因。

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

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

其他卡頓來源

嘗試執行 JavaScript 驅動的動畫時,最大的問題在於其他內容可能會干擾 rAF 回呼,甚至完全阻止其執行。即使 rAF 回呼很精簡,且只需幾毫秒就能執行,其他活動 (例如處理剛傳入的 XHR、執行輸入事件處理常式,或執行計時器上的排定更新) 仍可能突然出現,並在不中斷的情況下執行一段時間。在行動裝置上,處理這些事件有時可能需要數百毫秒,在這段期間,動畫會完全停滯。我們將這些動畫卡頓稱為資源浪費。

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

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

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

CSS 動畫

在事件和 rAF 回呼中,有什麼比輕量 JS 更好?不使用 JS。

我們在前面提過,沒有任何萬靈丹可以避免中斷 rAF 回呼,但你可以使用 CSS 動畫,完全避免需要回呼。特別是在 Android 版 Google Chrome (其他瀏覽器也正在開發類似功能),CSS 動畫具有非常理想的屬性,即使 JavaScript 正在執行,瀏覽器通常也能執行 CSS 動畫。

上述關於卡頓的部分隱含一項聲明:瀏覽器一次只能執行一項操作。雖然這並非絕對,但可以做為良好的工作假設:在任何指定時間,瀏覽器可以執行 JS、執行版面配置或繪製,但一次只能執行一項。您可以在開發人員工具的時間軸檢視畫面中確認這一點。這項規則的例外狀況之一,是 Android 版 Google Chrome (以及即將推出的電腦版 Chrome,但目前尚未推出) 的 CSS 動畫。

建議盡可能使用 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 版 Google 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. 製作動畫時,為每次螢幕重新整理產生影格至關重要。垂直同步動畫對應用程式的體驗有很大的正面影響。
  2. 如要在 Chrome 和其他新式瀏覽器中取得垂直同步動畫,最佳做法是使用 CSS 動畫。如果需要比 CSS 動畫更靈活的技術,最適合的是以 requestAnimationFrame 為基礎的動畫。
  3. 為確保 rAF 動畫順暢運作,請確認其他事件處理常式不會干擾 rAF 回呼執行,並縮短 rAF 回呼時間 (<15 毫秒)。

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

祝您動畫製作順利!

參考資料