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

Tom Wiltzius
Tom Wiltzius

簡介

您希望網頁應用程式在執行動畫、轉場效果和其他小型 UI 效果時,能夠提供流暢的體驗。若確認這些影響不會卡頓,或是讓人感同身受

這是一系列文章,主題是關於瀏覽器轉譯效能最佳化的文章。首先,我們將說明流暢動畫的重要性、達成這個目標的必要性,以及幾個簡單的最佳做法。其中許多想法最初都是用在《Jank Busters》Nat Duca 演講,以及我今年在 Google I/O 大會的演講 (影片)。

隆重推出 V 同步

電腦遊戲玩家可能很熟悉這個術語,但網路上也不太常見:什麼是 v-sync

考量手機的螢幕:定期更新,但通常 (但並非一定!) 每秒會重新整理大約 60 次。V 同步 (或垂直同步處理) 是指只在畫面重新整理之間產生新影格的做法。您可能會想成在程序將資料寫入螢幕緩衝區的程序,以及作業系統讀取該資料並放入顯示畫面之間的競爭狀況。我們希望緩衝影格內容在兩次重新整理之間變更,而非期間;否則螢幕將顯示一個畫面的一半大小,最後顯示「撕裂」

您需要在每次重新整理畫面時準備好新的影格,才能產生流暢的動畫。這會帶來兩個重大影響:影格時間 (也就是影格需要準備的時間) 和影格預算 (瀏覽器產生影格的時間)。螢幕重新整理的每次時間只有完成一個影格 (60Hz 螢幕縮短為 16 毫秒),而您想要在畫面上顯示最後一個影格時,立即開始產生下一個影格。

時間就是一切:requestAnimationFrame

許多網頁程式每 16 毫秒會使用 setIntervalsetTimeout 來建立動畫。這個問題的原因很多 (我們將稍後會進一步說明),但特別值得注意的是:

  • JavaScript 計時器的解析時間僅有幾毫秒
  • 各裝置的刷新率各不相同

請回想一下上述的影格時間問題:您必須先完成動畫影格,並備妥任何 JavaScript、DOM 操作、版面配置、繪製等作業,才能在下一個畫面重新整理前準備就緒。計時器解析度偏低可能會導致在下個畫面重新整理前,難以完成動畫影格,但螢幕刷新率的變化,無法搭配固定的計時器來完成。無論計時器的間隔時間為何,都會慢慢離開影格時間範圍,最後就會捨棄影格。即使計時器以毫秒為單位觸發 (如同開發人員發現),計時器解析度也會因電腦已開啟或接上電源而不同,還是會受到背景分頁耗用資源的影響等影響。即使計時器很少發生 (例如因為時間一秒才關閉,每 16 個影格),就會發生數次。您也會負責產生不再顯示的影格,浪費電力和 CPU 時間,因而浪費了應用程式的其他工作。

不同螢幕的刷新率各有不同:60Hz 相當常見,但某些手機採用 59 Hz 的刷新率,部分筆電在低功耗模式下降至 50 Hz,部分桌上型電腦的刷新率為 70 Hz。

討論轉譯效能時,我們往往著重在每秒影格數 (FPS),但變異值或許更嚴重。我們眼見到動畫中出現微小、不規律的撞擊,而動畫不及時效性。

requestAnimationFrame 是取得正確計時動畫影格的方法。使用這個 API 時,您將要求瀏覽器提供動畫影格。當瀏覽器即將產生新頁框時,系統就會呼叫您的回呼。無論重新整理頻率為何,都需符合上述條件。

requestAnimationFrame 還提供其他實用屬性:

  • 背景分頁中的動畫會暫停,節省系統資源和電池續航力。
  • 如果系統無法依螢幕刷新率處理轉譯作業,就能節流動畫,並降低迴呼頻率 (例如在 60Hz 螢幕上每秒 30 倍)。雖然這個做法只讓畫面更新率減半,但動畫始終維持一致。如上文所述,我們眼見的高度比畫面更新率更高。穩定的 30Hz 刷新率比 60Hz 好看,每秒不到幾個影格。

有關於「requestAnimationFrame」的討論主題,請參閱廣告素材 JS 相關文章,進一步瞭解相關資訊,但還是要將動畫流暢呈現,也是不可或缺的第一步。

影格預算

由於我們希望在每次重新整理畫面時都能準備新的影格,因此每次重新整理之間,系統只會產生所有工作建立新影格的時間。在 60Hz 螢幕上,這表示我們擁有大約 16 毫秒的時間來執行所有 JavaScript、執行版面配置、繪製作業,以及瀏覽器為了顯示外框而執行的其他動作。換句話說,如果 requestAnimationFrame 回呼中的 JavaScript 執行時間超過 16 毫秒,您就不需要為 v 同步產生影格時間!

16 毫秒才不會很久。幸好,Chrome 的開發人員工具可協助您追蹤在 requestAnimationFrame 回呼期間消耗影格預算的情形。

開啟開發人員工具時間軸,並快速錄製這個動畫的實際情形,顯示動畫效果超出預算。在時間軸中切換至「影格」並參閱:

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

這些 requestAnimationFrame (rAF) 回呼花費的時間超過 200 毫秒。這是每隔 16 毫秒就爆破一個影格的規模過長!開啟其中一個較長的 rAF 回呼,就能瞭解內部情況:在本例中為大量版面配置。

Paul 的影片詳細介紹了重新版面配置的具體原因 (內容是 scrollTop),並說明如何避免這個問題。但重點是您可以深入瞭解回呼,並調查耗時過長的內容。

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

請注意,影格時間為 16 毫秒。影格中的空白空間是處理更多作業的改善空間 (或者讓瀏覽器在背景執行工作)。這片空白是好事,

Jank 的其他來源

嘗試執行 JavaScript 技術的動畫時,發生問題的主要原因 就是其他東西可能會幹擾 rAF 回呼,甚至還能 完全不必執行即使 rAF 回呼非常精簡,只需幾個項目執行 其他活動 (例如處理剛傳入的 XHR, 執行輸入事件處理常式,或是對計時器執行排定的更新),可以 突然出現很短的時段 卻沒有產生收益使用行動裝置 有時需要數百毫秒才能處理這些事件 在這段期間內,動畫會完全停滯我們稱之為 動畫達成 jank

要避免這些情況,目前仍無法採取魔法,但可以參考以下幾項架構最佳做法,奠定成功基礎:

  • 請勿在輸入處理常式中執行大量處理作業!重複執行大量 JS,或嘗試重新排列整個網頁 (例如onscroll 處理常式是造成卡頓問題最常見的原因。
  • 請盡可能將處理程序 (讀取:任何耗時較長的內容) 推送到 rAF 回呼或網路工作處理序
  • 如果您將工作推送至 rAF 回呼,請嘗試分割畫面,這樣就只會處理每個影格的片段,或延遲到重要動畫播放完畢後再執行;如此一來,您就可以繼續執行短 rAF 回呼,並順暢播放動畫。

如需如何將處理程序推送至 requestAnimationFrame 回呼而非輸入處理常式,請參閱 Paul Lewis 的「Leaner, Meaner, Faster Animations with requestAnimationFrame」。

CSS 動畫

事件和 rAF 回呼比起輕量版 JS 有什麼優點?沒有 JS。

我們先前提過,可避免干擾 rAF 回呼作業的成真,但您可以使用 CSS 動畫來完全省去麻煩。在 Chrome for Android 中 (以及其他瀏覽器可以正常執行的功能),CSS 動畫即使正在執行 JavaScript,瀏覽器通常也能執行 CSS 動畫。

在卡頓的上一節中有個隱含陳述:瀏覽器一次只能執行一項操作。這不明確,但最好先考慮:瀏覽器在任何情況下都可執行 JS、執行版面配置或繪製,但一次只能執行一個假設。你可以在開發人員工具的時間軸檢視畫面確認這項資訊。但這項規定的其中一個例外是 Chrome for Android 上的 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 動畫,甚至捲動瀏覽靜態頁面。在本系列的下一篇文章中,我們將透過這些概念,探討捲動效能。

祝您製作動畫愉快!

參考資料