簡介
HTML5 提供我們絕佳的工具,可用於強化網路應用程式的視覺外觀。這在動畫領域中尤其重要。然而,這項新能力也面臨了新的挑戰。然而,這些挑戰其實並不是新的,有時也許可以請你關心的鄰居 (Flash 程式設計師) 看看她如何克服過往的類似問題。
總之,您在製作動畫時,請務必讓使用者覺得這些動畫要流暢。我們必須瞭解,光是提高每秒影格數,並不容易造成動畫的流暢度。不幸的是,我們的大腦已經比它聰明。您將瞭解到,真實 30 影格的每秒動畫數 (fps) 遠比 60 fps,而且中間只有幾個影格遭到捨棄。大家都很討厭鋸齒,
本文將提供工具和技術,協助您提升自家應用程式的使用體驗。
策略
當然不是,我們不希望您使用 HTML5 製作出色的視覺應用程式。
接著,如果您發現效能可以稍微提升,請返回這裡,繼續閱讀如何改善應用程式元素。當然,您可以一開始就完成一些工作,但絕對不要妨礙您的生產力。
透過 HTML5 呈現視覺擬真度 ++
硬體加速
硬體加速是瀏覽器整體轉譯效能的重要里程碑。一般做法是將主要 CPU 可能會計算的工作卸載至電腦的圖形處理器 (GPU) 中。這麼做不僅能大幅提升效能,還可以減少行動裝置上的資源消耗。
GPU 可以加速文件的這些面向
- 一般版面配置組合
- CSS3 轉場效果
- CSS3 3D 轉換
- 畫布繪圖
- WebGL 3D 繪圖
雖然畫布和 WebGL 的加速功能都是特殊用途的功能,但不一定適用於您的應用程式。前三個層面對所有應用程式的執行速度都將有所提升。
哪些項目可加速?
GPU 加速功能會將明確定義具體的工作卸載到特殊用途的硬體。一般的原則是將文件細分為多個「圖層」,而且這些圖層與網頁中的加速元素不會有太大差異。這些層會使用傳統的算繪管道進行算繪。接著,GPU 會將這些層合成單一頁面,藉此套用可以即時加速的「效果」。可能結果:在動畫發生時,在畫面上動畫的物件不需要單一「重新版面配置」頁面。
需要克服哪些挑戰,才能讓轉譯引擎輕鬆確認何時該套用具 GPU 加速功能。 請參考以下範例:
雖然這項動作可以順利運作,但瀏覽器無法判斷您要執行的是人類所認為是流暢動畫的項目。請思考改用 CSS3 轉場效果,達到同樣的視覺效果時,會發生什麼事:
開發人員已完全隱藏瀏覽器實作此動畫的方式。如此一來,瀏覽器就能運用 GPU 加速等技巧來達成定義目標。
有兩種實用的 Chrome 指令列旗標可協助對 GPU 加速進行偵錯:
--show-composited-layer-borders
會在 GPU 層級處理的元素周圍顯示紅色邊框。很適合確認您在 GPU 層內進行操弄。--show-paint-rects
會繪製所有非 GPU 變更,因此會在已重新繪製的所有區域周圍擲回淺色邊框。可以看出瀏覽器最佳化繪製區域的實際運作情形。
Safari 的執行階段標記也類似,請參閱這裡的說明。
CSS3 轉場效果
CSS Transitions 讓樣式動畫適合所有人使用,但這種效果更加智慧。CSS 轉場效果是由瀏覽器管理,因此動畫的擬真度會大幅提升,且在許多情況下也使用硬體加速。目前 WebKit (Chrome、Safari、iOS) 具備硬體加速 CSS 轉換功能,但其他瀏覽器和平台仍很快就能使用。
您可以使用 transitionEnd
事件,將上述內容編寫為強而有力的組合。不過,目前系統會擷取所有支援的轉場結束事件,也就是觀察 webkitTransitionEnd transitionend oTransitionEnd
。
許多程式庫現已導入動畫 API,這些 API 會在出現時利用轉場效果,在其他情況下改回使用標準 DOM 樣式動畫。scripty2、YUI 轉換、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 支援命名函式運算式並不常見。這麼做可讓它們在分析器中完美呈現。這項解決方案有一個問題:已命名的運算式實際上會將函式名稱放入目前的詞法範圍內。這個動作可能會破壞其他符號,請謹慎小心。
剖析長函式
假設您擁有很長的函式,並懷疑其中一小部分可能是效能問題的原因。有兩種方法可以找出問題所在:
- 正確方法:重構程式碼,避免加入任何長函式。
- 邪惡的建構完成方法:在程式碼中加入陳述式,並以已命名的自呼叫函式的形式加入陳述式。請注意,這不會改變語意,且讓函式的某些部分在分析器中顯示為個別函式:
js function myLongFunction() { ... (function doAPartOfTheWork() { ... })(); ... }
在完成剖析後,別忘了移除這些額外函式,甚至可用這些函式做為重構程式碼的起點。
DOM 剖析
最新的 Chrome 網頁檢查器開發工具包含新的「時間軸檢視畫面」,會顯示瀏覽器執行的低階操作時間軸。您可以參考這項資訊來最佳化 DOM 作業。建議盡量減少瀏覽器在執行程式碼時需要執行的「動作」次數。
時間軸檢視畫面可提供豐富的資訊。因此,您應嘗試建立可獨立執行的最小測試案例。
上圖顯示簡易指令碼的時間軸檢視畫面輸出內容。左側窗格會顯示瀏覽器執行的作業 (依時間順序排序),右側窗格中的時間軸則顯示個別作業實際消耗的時間。
進一步瞭解時間軸檢視畫面。在 Internet Explorer 中,您可以使用 DynaTrace Ajax 版本進行剖析。
剖析策略
單接切面
當您想要剖析應用程式時,請試著找出可能會盡量接近觸發速度緩慢情形的部分功能。接著,請嘗試執行設定檔,確保僅執行與應用程式這些方面相關的部分程式碼。這樣會使剖析資料更容易解讀,因為資料不會與與實際問題無關的程式碼路徑混合。以下列舉應用程式各個層面的良好範例:
- 啟動時間 (啟用分析器、重新載入應用程式、等待初始化完成,然後停止分析器。
- 點選按鈕和後續動畫 (啟動分析器、點選按鈕、等待動畫結束、停止分析器)。
GUI 剖析
在 GUI 程式中只執行應用程式的正確部分,可能比進行最佳化時更難執行,例如 3D 引擎的光線追蹤程式。舉例來說,如果您想要剖析按下某個按鈕後發生的內容,可能會一直觸發不相關的滑鼠懸停事件,導致結果較不明確。請盡量避免這種情況 :)
程式介面
您也可以透過程式介面啟用偵錯工具。如此一來,您就能精確控制剖析的開始和結束時間。
使用以下應用程式開始剖析:
console.profile()
停止剖析時使用:
console.profileEnd()
可重複性
剖析時,確保您可以確實重現結果。唯有如此,才能知道您的最佳化作業是否確實改善成效。此外,功能層級剖析是在整部電腦的環境中完成。這不是一門精確的科學。電腦上的許多其他活動可能會影響個別個人資料的運作情況:
- 您的應用程式中不相關的計時器,會在您測量其他項目時觸發。
- 垃圾收集器正在運作
- 瀏覽器中另一個分頁在同一作業執行緒中執行繁雜工作。
- 電腦中的其他程式佔用 CPU 的效能,導致應用程式速度變慢。
- 地表深處突然發生變化。
此外,在一個剖析工作階段中多次執行相同的程式碼路徑也很合理。這樣就能減少對上述因素的影響,慢速零件可能就比較容易明顯。
測量、改善、測量
如果您在程式中發現速度緩慢的部分,請試著思考改善執行行為的方法。變更程式碼後,請重新建立設定檔。如果對結果感到滿意,繼續進行,如果還是沒有改善,您應該復原變更,不要留下「因為不會改變」狀態。
最佳化策略
盡量減少 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()
會建立此映像檔:
改用更快速的導入方式會產生以下圖片:
這些圖片顯示,重新排列程式碼存取 DOM 的方式,可大幅提升轉譯效能。在這種情況下,原始程式碼必須重新計算樣式及版面配置兩次,才能產生相同的結果。基本上,所有「實際」程式碼都可以套用類似的最佳化功能,產生非常出色的結果。
閱讀完整內容:轉譯:重新繪製、重排/重新版面配置、restyle由 Stoyan Stefanov 繪製
重新繪製與事件迴圈
在瀏覽器中執行 JavaScript 時,採用的是「事件迴圈」模型。瀏覽器的預設狀態為「閒置」。這個狀態可能會因為使用者互動的事件或 JavaScript 計時器或 Ajax 回呼等狀況而中斷。每當 JavaScript 因中斷點而執行時,瀏覽器通常會等待程序完成,直到重新繪製畫面為止 (極長時間執行的 JavaScript 可能出現例外情況,例如因出現警示方塊而有效地中斷 JavaScript 執行的情況。
後果
- 如果執行 JavaScript 動畫循環時間超過 1/30 秒,您將無法建立流暢的動畫,因為瀏覽器在 JS 執行期間不會重新繪製。如果預期還要一併處理使用者事件,就需要可以大幅加快處理速度。
- 有時候,將部分 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,因為使用者想要忽略使用者體驗最佳做法的設計。如果希望網頁載入速度變快,請不要這麼做。請在最終格式中,從伺服器端傳送所有標記。這個情況再次發生許多問題,因此請思考速度是否值得取捨。
工具
- JSPerf - JavaScript 基準程式碼片段
- Firebug - 使用 Firefox 進行剖析
- Google Chrome 開發人員工具 (適用於 Safari 的 WebInspector)
- DOM Monster - 最佳化 DOM 效能
- DynaTrace Ajax 版:適用於 Internet Explorer 中的剖析與繪製最佳化