改善 HTML5 應用程式的效能

Malte Ubl
Malte Ubl

簡介

HTML5 提供我們絕佳的工具,可用於強化網路應用程式的視覺外觀。這在動畫領域中尤其重要。然而,這項新能力也面臨了新的挑戰。然而,這些挑戰其實並不是新的,有時也許可以請你關心的鄰居 (Flash 程式設計師) 看看她如何克服過往的類似問題。

總之,您在製作動畫時,請務必讓使用者覺得這些動畫要流暢。我們必須瞭解,光是提高每秒影格數,並不容易造成動畫的流暢度。不幸的是,我們的大腦已經比它聰明。您將瞭解到,真實 30 影格的每秒動畫數 (fps) 遠比 60 fps,而且中間只有幾個影格遭到捨棄。大家都很討厭鋸齒,

本文將提供工具和技術,協助您提升自家應用程式的使用體驗。

策略

當然不是,我們不希望您使用 HTML5 製作出色的視覺應用程式。

接著,如果您發現效能可以稍微提升,請返回這裡,繼續閱讀如何改善應用程式元素。當然,您可以一開始就完成一些工作,但絕對不要妨礙您的生產力。

透過 HTML5 呈現視覺擬真度 ++

硬體加速

硬體加速是瀏覽器整體轉譯效能的重要里程碑。一般做法是將主要 CPU 可能會計算的工作卸載至電腦的圖形處理器 (GPU) 中。這麼做不僅能大幅提升效能,還可以減少行動裝置上的資源消耗。

GPU 可以加速文件的這些面向

  • 一般版面配置組合
  • CSS3 轉場效果
  • CSS3 3D 轉換
  • 畫布繪圖
  • WebGL 3D 繪圖

雖然畫布和 WebGL 的加速功能都是特殊用途的功能,但不一定適用於您的應用程式。前三個層面對所有應用程式的執行速度都將有所提升。

哪些項目可加速?

GPU 加速功能會將明確定義具體的工作卸載到特殊用途的硬體。一般的原則是將文件細分為多個「圖層」,而且這些圖層與網頁中的加速元素不會有太大差異。這些層會使用傳統的算繪管道進行算繪。接著,GPU 會將這些層合成單一頁面,藉此套用可以即時加速的「效果」。可能結果:在動畫發生時,在畫面上動畫的物件不需要單一「重新版面配置」頁面。

需要克服哪些挑戰,才能讓轉譯引擎輕鬆確認何時該套用具 GPU 加速功能。 請參考以下範例:

雖然這項動作可以順利運作,但瀏覽器無法判斷您要執行的是人類所認為是流暢動畫的項目。請思考改用 CSS3 轉場效果,達到同樣的視覺效果時,會發生什麼事:

開發人員已完全隱藏瀏覽器實作此動畫的方式。如此一來,瀏覽器就能運用 GPU 加速等技巧來達成定義目標。

有兩種實用的 Chrome 指令列旗標可協助對 GPU 加速進行偵錯:

  1. --show-composited-layer-borders 會在 GPU 層級處理的元素周圍顯示紅色邊框。很適合確認您在 GPU 層內進行操弄。
  2. --show-paint-rects 會繪製所有非 GPU 變更,因此會在已重新繪製的所有區域周圍擲回淺色邊框。可以看出瀏覽器最佳化繪製區域的實際運作情形。

Safari 的執行階段標記也類似,請參閱這裡的說明

CSS3 轉場效果

CSS Transitions 讓樣式動畫適合所有人使用,但這種效果更加智慧。CSS 轉場效果是由瀏覽器管理,因此動畫的擬真度會大幅提升,且在許多情況下也使用硬體加速。目前 WebKit (Chrome、Safari、iOS) 具備硬體加速 CSS 轉換功能,但其他瀏覽器和平台仍很快就能使用。

您可以使用 transitionEnd 事件,將上述內容編寫為強而有力的組合。不過,目前系統會擷取所有支援的轉場結束事件,也就是觀察 webkitTransitionEnd transitionend oTransitionEnd

許多程式庫現已導入動畫 API,這些 API 會在出現時利用轉場效果,在其他情況下改回使用標準 DOM 樣式動畫。scripty2YUI 轉換jQuery 動畫增強

CSS3 翻譯

我相信您已經找到自己一直以來都能為頁面中某個元素的 x/y 位置加上動畫效果。您可能修改了內嵌樣式的 left 和 top 屬性。透過 2D 轉換,我們可以使用 translate() 功能來重現這個行為。

我們可以結合 DOM 動畫 來運用

<div style="position:relative; height:120px;" class="hwaccel">

  <div style="padding:5px; width:100px; height:100px; background:papayaWhip;
              position:absolute;" id="box">
  </div>
</div>

<script>
document.querySelector('#box').addEventListener('click', moveIt, false);

function moveIt(evt) {
  var elem = evt.target;

  if (Modernizr.csstransforms && Modernizr.csstransitions) {
    // vendor prefixes omitted here for brevity
    elem.style.transition = 'all 3s ease-out';
    elem.style.transform = 'translateX(600px)';

  } else {
    // if an older browser, fall back to jQuery animate
    jQuery(elem).animate({ 'left': '600px'}, 3000);
  }
}
</script>

我們使用 Modernizr 來對 CSS 2D Transforms 和 CSS Transitions 進行功能測試,以便使用平移功能來改變位置。如果此動畫是使用轉場效果的動畫,瀏覽器很可能是硬體加速。為了讓瀏覽器又往正確的方向推動,我們會使用上述的「放大鏡符號」。

如果瀏覽器功能較差,我們會改用 jQuery 來移動元素。您可以從 Louis-Remi Babe 挑選 jQuery Transform polyfill 外掛程式,將整個工作自動化。

window.requestAnimationFrame

requestAnimationFrame 是由 Mozilla 開發,並由 WebKit 推出。我們的目標是提供原生 API,以便執行動畫,無論動畫採用 DOM/CSS 語言,或採用 <canvas> 或 WebGL。瀏覽器可以將並行動畫最佳化,變成單一的重排和重新繪製循環,進而產生更高的保真度動畫。例如與 CSS 轉場效果或 SVG SMIL 同步處理的 JS 動畫。此外,如果在未顯示的分頁中執行動畫循環,瀏覽器就無法繼續執行,也就是減少 CPU、GPU 和記憶體用量,導致電池續航力變長。

如要進一步瞭解 requestAnimationFrame 的使用方式和使用原因,請參閱 Paul Ireland 的「requestAnimationFrame for smart animating」文章。

剖析

當您發現可以改善應用程式的速度時,就該深入剖析分析,找出最佳化調整可帶來最大效益的地方。最佳化作業通常會對原始碼的可維護性造成負面影響,因此只有在必要時才套用。剖析結果可讓您瞭解程式碼的哪些部分能改善效能,進而帶來最佳效益。

JavaScript 剖析

JavaScript 分析器會測量每個函式從開始到結束執行所需的時間,讓您大致瞭解應用程式在 JavaScript 函式層級的效能。

函式的總執行時間是指由上至下執行函式所需的總時間。淨執行時間是指總執行時間,減去執行函式呼叫函式所花費的時間。

某些函式的呼叫頻率較高。分析器通常會提供所有叫用的執行時間,以及平均和最短與最長執行時間。

詳情請參閱 Chrome 開發人員工具文件剖析

DOM

JavaScript 效能對應用程式的流動性與回應性極大影響。請特別注意,雖然 JavaScript 分析器會測量 JavaScript 的執行時間,但這些分析器也可以間接測量執行 DOM 作業的時間。這些 DOM 作業通常也是效能問題的核心,

function drawArray(array) {
  for(var i = 0; i < array.length; i++) {
    document.getElementById('test').innerHTML += array[i]; // No good :(
  }
}

例如,在上述程式碼中,幾乎沒有時間用來執行實際 JavaScript。DrawArray- 函式很有很可能會出現在設定檔中,因為 DrawArray- 函式會以非常浪費的方式與 DOM 互動。

提示與秘訣

匿名函式

匿名函式原本就沒有名稱會顯示在分析器中,因此不容易剖析。有兩種方法可以解決這個問題:

$('.stuff').each(function() { ... });

重寫為:

$('.stuff').each(function workOnStuff() { ... });

JavaScript 支援命名函式運算式並不常見。這麼做可讓它們在分析器中完美呈現。這項解決方案有一個問題:已命名的運算式實際上會將函式名稱放入目前的詞法範圍內。這個動作可能會破壞其他符號,請謹慎小心。

剖析長函式

假設您擁有很長的函式,並懷疑其中一小部分可能是效能問題的原因。有兩種方法可以找出問題所在:

  1. 正確方法:重構程式碼,避免加入任何長函式。
  2. 邪惡的建構完成方法:在程式碼中加入陳述式,並以已命名的自呼叫函式的形式加入陳述式。請注意,這不會改變語意,且讓函式的某些部分在分析器中顯示為個別函式: js function myLongFunction() { ... (function doAPartOfTheWork() { ... })(); ... } 在完成剖析後,別忘了移除這些額外函式,甚至可用這些函式做為重構程式碼的起點。

DOM 剖析

最新的 Chrome 網頁檢查器開發工具包含新的「時間軸檢視畫面」,會顯示瀏覽器執行的低階操作時間軸。您可以參考這項資訊來最佳化 DOM 作業。建議盡量減少瀏覽器在執行程式碼時需要執行的「動作」次數。

時間軸檢視畫面可提供豐富的資訊。因此,您應嘗試建立可獨立執行的最小測試案例。

DOM 剖析

上圖顯示簡易指令碼的時間軸檢視畫面輸出內容。左側窗格會顯示瀏覽器執行的作業 (依時間順序排序),右側窗格中的時間軸則顯示個別作業實際消耗的時間。

進一步瞭解時間軸檢視畫面。在 Internet Explorer 中,您可以使用 DynaTrace Ajax 版本進行剖析。

剖析策略

單接切面

當您想要剖析應用程式時,請試著找出可能會盡量接近觸發速度緩慢情形的部分功能。接著,請嘗試執行設定檔,確保僅執行與應用程式這些方面相關的部分程式碼。這樣會使剖析資料更容易解讀,因為資料不會與與實際問題無關的程式碼路徑混合。以下列舉應用程式各個層面的良好範例:

  1. 啟動時間 (啟用分析器、重新載入應用程式、等待初始化完成,然後停止分析器。
  2. 點選按鈕和後續動畫 (啟動分析器、點選按鈕、等待動畫結束、停止分析器)。
GUI 剖析

在 GUI 程式中只執行應用程式的正確部分,可能比進行最佳化時更難執行,例如 3D 引擎的光線追蹤程式。舉例來說,如果您想要剖析按下某個按鈕後發生的內容,可能會一直觸發不相關的滑鼠懸停事件,導致結果較不明確。請盡量避免這種情況 :)

程式介面

您也可以透過程式介面啟用偵錯工具。如此一來,您就能精確控制剖析的開始和結束時間。

使用以下應用程式開始剖析:

console.profile()

停止剖析時使用:

console.profileEnd()

可重複性

剖析時,確保您可以確實重現結果。唯有如此,才能知道您的最佳化作業是否確實改善成效。此外,功能層級剖析是在整部電腦的環境中完成。這不是一門精確的科學。電腦上的許多其他活動可能會影響個別個人資料的運作情況:

  1. 您的應用程式中不相關的計時器,會在您測量其他項目時觸發。
  2. 垃圾收集器正在運作
  3. 瀏覽器中另一個分頁在同一作業執行緒中執行繁雜工作。
  4. 電腦中的其他程式佔用 CPU 的效能,導致應用程式速度變慢。
  5. 地表深處突然發生變化。

此外,在一個剖析工作階段中多次執行相同的程式碼路徑也很合理。這樣就能減少對上述因素的影響,慢速零件可能就比較容易明顯。

測量、改善、測量

如果您在程式中發現速度緩慢的部分,請試著思考改善執行行為的方法。變更程式碼後,請重新建立設定檔。如果對結果感到滿意,繼續進行,如果還是沒有改善,您應該復原變更,不要留下「因為不會改變」狀態。

最佳化策略

盡量減少 DOM 互動

提升網路用戶端應用程式速度的另一個主題,就是將 DOM 互動降到最低。雖然 JavaScript 引擎的速度越來越高,但存取 DOM 的速度仍持不相同。這也是因為幾乎沒有實際的原因 (例如,在畫面中版面配置和繪製內容只是時間)。

快取 DOM 節點

從 DOM 擷取節點或節點清單時,請思考是否能在後續的運算中重複使用節點或節點清單 (甚至是下一個迴圈疊代)。只要您並未實際在相關區域中新增或刪除節點,通常都是如此。

彙整前:

function getElements() {
  return $('.my-class');
}

更新後:

var cachedElements;
function getElements() {
  if (cachedElements) {
    return cachedElements;
  }
  cachedElements = $('.my-class');
  return cachedElements;
}

快取屬性值

如同快取 DOM 節點,您也可以快取屬性值。假設您要為節點樣式的屬性建立動畫效果。如果您知道自己 (在程式碼中的該部分) 是唯一會碰到該屬性的,可以在每次疊代時快取最後一個值,這樣就不必重複讀取。

彙整前:

setInterval(function() {
  var ele = $('#element');
  var left = parseInt(ele.css('left'), 10);
  ele.css('left', (left + 5) + 'px');
}, 1000 / 30);

變更後: js var ele = $('#element'); var left = parseInt(ele.css('left'), 10); setInterval(function() { left += 5; ele.css('left', left + 'px'); }, 1000 / 30);

將 DOM 操縱他人移出迴圈

迴圈通常是最佳化的熱點。請試著思考要搭配 DOM 使用時,要如何分離實際數字。您通常可以計算,等完成後一次套用所有結果。

彙整前:

document.getElementById('target').innerHTML = '';
for(var i = 0; i < array.length; i++) {
  var val = doSomething(array[i]);
  document.getElementById('target').innerHTML += val;
}

更新後:

var stringBuilder = [];
for(var i = 0; i < array.length; i++) {
  var val = doSomething(array[i]);
  stringBuilder.push(val);
}
document.getElementById('target').innerHTML = stringBuilder.join('');

重繪和重排

如先前所述,存取 DOM 的速度相對慢。當程式碼讀取您必須重新計算的值時,因為程式碼最近修改了 DOM 中相關的內容,所以呼叫速度會變得非常緩慢。因此,請避免混音來讀取及寫入 DOM。在理想情況下,程式碼應一律分為兩個階段:

  • 階段 1:讀取程式碼所需的 DOM 值
  • 第 2 階段:修改 DOM

盡量不要編寫下列模式:

  • 階段 1:讀取 DOM 值
  • 第 2 階段:修改 DOM
  • 第 3 階段:閱讀更多資訊
  • 第 4 階段:在其他位置修改 DOM。

彙整前:

function paintSlow() {
  var left1 = $('#thing1').css('left');
  $('#otherThing1').css('left', left);
  var left2 = $('#thing2').css('left');
  $('#otherThing2').css('left', left);
}

更新後:

function paintFast() {
  var left1 = $('#thing1').css('left');
  var left2 = $('#thing2').css('left');
  $('#otherThing1').css('left', left);
  $('#otherThing2').css('left', left);
}

此建議適用於在單一 JavaScript 執行情境中發生的動作。(例如在事件處理常式內、間隔處理常式內,或是處理 ajax 回應時)。

執行上述函式 paintSlow() 會建立此映像檔:

paintSlow()

改用更快速的導入方式會產生以下圖片:

加速導入技術

這些圖片顯示,重新排列程式碼存取 DOM 的方式,可大幅提升轉譯效能。在這種情況下,原始程式碼必須重新計算樣式及版面配置兩次,才能產生相同的結果。基本上,所有「實際」程式碼都可以套用類似的最佳化功能,產生非常出色的結果。

閱讀完整內容:轉譯:重新繪製、重排/重新版面配置、restyle由 Stoyan Stefanov 繪製

重新繪製與事件迴圈

在瀏覽器中執行 JavaScript 時,採用的是「事件迴圈」模型。瀏覽器的預設狀態為「閒置」。這個狀態可能會因為使用者互動的事件或 JavaScript 計時器或 Ajax 回呼等狀況而中斷。每當 JavaScript 因中斷點而執行時,瀏覽器通常會等待程序完成,直到重新繪製畫面為止 (極長時間執行的 JavaScript 可能出現例外情況,例如因出現警示方塊而有效地中斷 JavaScript 執行的情況。

後果

  1. 如果執行 JavaScript 動畫循環時間超過 1/30 秒,您將無法建立流暢的動畫,因為瀏覽器在 JS 執行期間不會重新繪製。如果預期還要一併處理使用者事件,就需要可以大幅加快處理速度。
  2. 有時候,將部分 JavaScript 動作延遲至稍後一點會很有幫助。例如:setTimeout(function() { ... }, 0) 這個方法實際上會指示瀏覽器在事件迴圈再次閒置時,立即執行回呼 (在某些情況下,部分瀏覽器會等待至少 10 毫秒)。請記住,這會建立兩個時間非常接近的 JavaScript 執行週期。兩者都會觸發畫面重新繪製作業,因此完成繪畫作業的總時間可能會加倍。實際觸發兩次繪製作業取決於瀏覽器的經驗法則。

一般版本:

function paintFast() {
  var height1 = $('#thing1').css('height');
  var height2 = $('#thing2').css('height');
  $('#otherThing1').css('height', '20px');
  $('#otherThing2').css('height', '20px');
}
重新繪製與事件迴圈

讓我們增加一些延遲:

function paintALittleLater() {
  var height1 = $('#thing1').css('height');
  var height2 = $('#thing2').css('height');
  $('#otherThing1').css('height', '20px');
  setTimeout(function() {
    $('#otherThing2').css('height', '20px');
  }, 10)
}
延遲時間

延遲版本顯示瀏覽器會繪製兩次,但對網頁進行的兩次變更只有一半。

延遲初始化

使用者都希望能夠快速載入、回應迅速的網頁應用程式。然而,使用者對於「緩慢」的看法,視其動作而定。例如,應用程式不應在滑鼠遊標懸停事件上進行大量運算,因為這樣可能會在使用者繼續移動滑鼠時造成不良的使用者體驗。不過,當使用者點選按鈕後,習慣會稍微延遲。

因此,建議您盡快移動要執行的初始化程式碼 (例如使用者點選啟動應用程式特定元件的按鈕時)。

變更前: js var things = $('.ele > .other * div.className'); $('#button').click(function() { things.show() });

變更後: js $('#button').click(function() { $('.ele > .other * div.className').show() });

事件委派

在網頁上分散事件處理常式可能花費的時間相當長,而且在動態替換元素後,這些處理常式就需要將事件處理常式重新附加至新元素,因此相當繁瑣。

這種情況的解決方法是使用事件委派技術。比起將個別事件處理常式附加至元素,系統會實際將事件處理常式附加至父項節點,並檢查事件的目標節點來確認事件是否感興趣,而非將個別事件處理常式附加至元素。

在 jQuery 中,這可以輕鬆表示:

$('#parentNode').delegate('.button', 'click', function() { ... });

不使用事件委派功能的時機

有時則相反的是,您使用的是事件委派功能,遇到效能問題。基本上,事件委派可讓您保持複雜度的初始化時間。不過,每次叫用該事件都必須支付相關費用,確認特定事件是否需要付費。費用可能高昂,尤其是經常發生的「滑鼠移動」或「滑鼠移動」等活動。

一般問題和解決方案

我在$(document).ready中進行的操作需要很長的時間

Malte 的個人建議:請勿在 $(document).ready 中執行任何動作。請嘗試以最終形式提交文件。好的,您可以註冊事件監聽器,但只能使用 id-selector 和/或使用事件委派。如果是「mousemove」等昂貴的事件,請延後註冊直到有需要為止 (相關元素上的滑鼠遊標懸停事件)。

如果確實有必要執行動作,例如送出 Ajax 要求以取得實際資料,請顯示精美動畫;如果是 GIF 動畫或類似效果,建議您將動畫加入為資料 URI。

我在網頁上加入 Flash 電影後,運作速度非常緩慢

在網頁中加入 Flash 之後,其轉譯速度一律會稍微變慢,因為最後視窗版面配置必須在瀏覽器和 Flash 外掛程式之間進行「協商」。如果無法完全避免在網頁上放置 Flash,請務必把「wmode」Flash 參數設為「window」(預設值)。此功能會停用複合 HTML 和 Flash 元素的功能 (您無法看到位於 Flash 影片上方的 HTML 元素,而且您的 Flash 影片不得設為透明)。這樣或許會造成不便,但可大幅改善成效。例如仔細思考 youtube.com 如何避免在主要電影播放器上方放置圖層。

我正在將內容儲存到 localStorage,應用程式也會延遲

寫入 localStorage 是需要啟動硬碟的同步作業。您不希望在執行動畫時進行「長時間執行」同步作業。將 localStorage 的存取權移至程式碼中的位置,您需要確保使用者處於閒置狀態且沒有播放動畫。

剖析點到 jQuery 選取器的速度非常緩慢

首先,請確認選取器可以透過 document.querySelectorAll 執行。您可以在 JavaScript 控制台中測試這一點。如果出現例外狀況,重新編寫選擇器,改為不要使用 JavaScript 架構的任何特殊擴充功能。這可按照規模大小,加快新瀏覽器中的選取器速度。

如果問題仍未解決,或是您希望加快在新版瀏覽器中的載入速度,請遵守下列規範:

  • 盡可能明確說明選取器右側。
  • 請使用不常使用的代碼名稱做為最右側的選取器部分。
  • 如果沒有幫助,可以考慮重新編寫內容,以便使用 ID 選取器

這些 DOM 操作需要很長的時間

許多 DOM 節點會插入、移除和更新,執行速度可能相當緩慢。一般而言,只要產生大量 HTML 字串,並使用 domNode.innerHTML = newHTML 取代舊內容,就能進行最佳化。請注意,這樣做可能會使系統難以維護,並在 IE 中建立記憶體連結,因此請小心。

另一個常見的問題是,初始化程式碼可能會建立大量 HTML。例如 jQuery 外掛程式,可將特定方塊轉換為多個 div,因為使用者想要忽略使用者體驗最佳做法的設計。如果希望網頁載入速度變快,請不要這麼做。請在最終格式中,從伺服器端傳送所有標記。這個情況再次發生許多問題,因此請思考速度是否值得取捨。

工具

  1. JSPerf - JavaScript 基準程式碼片段
  2. Firebug - 使用 Firefox 進行剖析
  3. Google Chrome 開發人員工具 (適用於 Safari 的 WebInspector)
  4. DOM Monster - 最佳化 DOM 效能
  5. DynaTrace Ajax 版:適用於 Internet Explorer 中的剖析與繪製最佳化

其他資訊

  1. Google 速度飛快
  2. Paul Ireland 的 jQuery 效能
  3. 極致 JavaScript 效能 (投影片簡報)