無論開發哪種應用程式,最佳化效能、確保快速載入及提供流暢互動體驗,對於使用者體驗和應用程式的成功至關重要。其中一種做法是使用剖析工具檢查應用程式的活動,查看在時間範圍內執行時的幕後運作情形。開發人員工具中的「效能」面板是絕佳的剖析工具,可分析及最佳化網路應用程式的效能。如果應用程式在 Chrome 中執行,您可以詳細瞭解瀏覽器在執行應用程式時的運作方式。瞭解這項活動有助於找出模式、瓶頸和效能熱點,進而採取行動提升效能。
以下範例將逐步說明如何使用「效能」面板。
設定及重新建立剖析情境
我們最近設定了目標,要讓「效能」面板的效能更上一層樓。尤其是要加快載入大量成效資料的速度。舉例來說,在分析長時間執行的複雜程序,或是擷取高精細度資料時,就會發生這種情況。為此,我們首先需要瞭解應用程式的執行方式和原因,而這可透過剖析工具達成。
如您所知,開發人員工具本身就是網頁應用程式。因此,您可以使用「效能」面板進行分析。如要分析這個面板本身,可以開啟開發人員工具,然後開啟附加至該工具的另一個開發人員工具執行個體。在 Google,這項設定稱為「開發人員工具中的開發人員工具」。
設定完成後,必須重新建立並記錄要分析的案例。為避免混淆,原始的開發人員工具視窗稱為「第一個開發人員工具執行個體」,檢查第一個執行個體的視窗則稱為「第二個開發人員工具執行個體」。

在第二個開發人員工具執行個體中,「效能」面板 (以下簡稱「perf 面板」) 會觀察第一個開發人員工具執行個體,重現載入設定檔的情境。
在第二個 DevTools 執行個體中,系統會開始即時記錄,而第一個執行個體則會從磁碟上的檔案載入設定檔。載入大型檔案,以便準確剖析處理大型輸入內容的效能。兩個執行個體都載入完畢後,您會在第二個開發人員工具執行個體中看到效能剖析資料 (通常稱為「追蹤」),效能面板會載入設定檔。
初始狀態:找出改善機會
載入完成後,我們在下一個螢幕截圖中觀察到第二個效能面板執行個體上的以下內容。請著重於主要執行緒的活動,這會顯示在標示為「Main」(主要) 的軌跡下方。從火焰圖中可以看出,活動可分為五大群組。這些工作是載入時間最長的工作。這些工作總共約耗費 10 秒。在下方的螢幕截圖中,我們使用「成效」面板,專注於這些活動群組,看看能找到什麼。

第一組活動:不必要的工作
很明顯地,第一組活動是仍在執行的舊版程式碼,但實際上並不需要。基本上,標示為 processThreadEvents
的綠色方塊下方所有內容都是白費力氣。這項做法可快速收穫成效。移除該函式呼叫後,節省了約 1.5 秒的時間。太棒了!
第二個活動群組
第二個活動群組的解決方案不像第一個那麼簡單。buildProfileCalls
大約耗費 0.5 秒,而且這項工作無法避免。

出於好奇,我們在效能面板中啟用「記憶體」選項,進一步調查後發現 buildProfileCalls
活動也使用了大量記憶體。如圖所示,藍線圖在 buildProfileCalls
執行時突然跳動,這表示可能發生記憶體洩漏。

為追蹤這項疑慮,我們使用「記憶體」面板 (DevTools 中的另一個面板,與效能面板中的「記憶體」抽屜不同) 進行調查。在「記憶體」面板中,系統選取了「分配取樣」剖析類型,因此記錄了效能面板載入 CPU 剖析時的堆積快照。

下列螢幕截圖顯示收集到的堆積快照。

從這個堆積快照中,我們發現 Set
類別耗用大量記憶體。檢查呼叫點後,我們發現自己不必要地將 Set
型別的屬性指派給大量建立的物件。這項成本不斷增加,且耗用大量記憶體,導致應用程式經常因輸入內容過大而當機。
集合適用於儲存不重複的項目,並提供可運用內容不重複性的作業,例如重複資料刪除資料集,以及提供更有效率的查閱作業。不過,由於儲存的資料保證與來源資料不同,因此不需要這些功能。因此一開始就不需要集合。為改善記憶體配置,屬性類型已從 Set
變更為一般陣列。套用這項變更後,系統會再次擷取堆積快照,並觀察到記憶體配置減少。雖然這項變更並未大幅提升速度,但次要好處是應用程式當機的頻率降低了。

第三個活動群組:權衡資料結構的取捨
第三個部分很特別:您可以在火焰圖中看到,這個部分是由細長直欄組成,表示函式呼叫很深,在這個案例中,遞迴也很深。這個部分總共持續約 1.4 秒。查看這個部分的底部後,很明顯這些資料欄的寬度是由一個函式的時間長度決定:appendEventAtLevel
,這表示可能是瓶頸
在 appendEventAtLevel
函式的實作內容中,有一件事特別突出。對於輸入內容中的每個資料項目 (在程式碼中稱為「事件」),系統都會在追蹤時間軸項目垂直位置的地圖中新增項目。這會造成問題,因為儲存的項目數量非常龐大。地圖可快速執行以鍵為準的查閱作業,但這項優勢並非免費。隨著地圖變大,新增資料可能會因重新雜湊而變得昂貴。如果連續在地圖上新增大量項目,這項成本就會變得明顯。
/**
* Adds an event to the flame chart data at a defined vertical level.
*/
function appendEventAtLevel (event, level) {
// ...
const index = data.length;
data.push(event);
this.indexForEventMap.set(event, index);
// ...
}
我們嘗試了另一種做法,不必為火焰圖中的每個項目在地圖中新增項目。這項改善效果顯著,證實瓶頸確實與將所有資料新增至地圖時產生的額外負荷有關。活動群組耗費的時間從約 1.4 秒縮短至約 200 毫秒。
變更前:

變更後:

第四個活動群組:延後非重要工作並快取資料,避免重複工作
放大這個視窗後,可以看到有兩組幾乎完全相同的函式呼叫區塊。從呼叫的函式名稱,您可以推斷這些區塊是由建構樹狀結構的程式碼組成 (例如名稱為 refreshTree
或 buildChildren
)。事實上,相關程式碼會在面板的底部抽屜中建立樹狀結構檢視畫面。有趣的是,這些樹狀檢視畫面不會在載入後立即顯示,使用者必須選取樹狀檢視畫面 (抽屜中的「由下而上」、「呼叫樹狀結構」和「事件記錄」分頁),系統才會顯示樹狀結構。此外,如螢幕截圖所示,樹狀結構建構程序執行了兩次。

我們發現這張圖片有兩個問題:
- 某項非必要工作阻礙了載入時間的效能。使用者不一定需要這項輸出內容。因此,這項工作對載入設定檔並不重要。
- 這些工作結果未快取。因此,即使資料沒有變更,系統仍會計算兩次樹狀結構。
我們首先將樹狀結構計算作業延後到使用者手動開啟樹狀結構檢視畫面時執行。這樣才值得付出建立這些樹狀結構的代價。執行兩次這項作業的總時間約為 3.4 秒,因此延遲載入可大幅縮短載入時間。我們仍在研究是否要快取這類工作。
第五個活動群組:盡可能避免複雜的呼叫階層
仔細查看這個群組後,我們發現特定呼叫鏈會重複叫用。火焰圖中不同位置出現了 6 次相同模式,這個視窗的總時間長度約為 2.4 秒!

系統會多次呼叫相關程式碼,處理要在「迷你地圖」(面板頂端的時間軸活動總覽) 上顯示的資料。我們不清楚為何會發生多次,但肯定不需要發生 6 次!事實上,如果沒有載入其他設定檔,程式碼的輸出內容應會維持目前狀態。理論上,程式碼只會執行一次。
調查結果顯示,載入管道中的多個部分直接或間接呼叫了計算迷你地圖的函式,導致相關程式碼遭到呼叫。這是因為程式的呼叫圖複雜度會隨時間演變,而且不知不覺中會新增更多程式碼的依附元件。這個問題無法快速解決。解決方式取決於相關程式碼集的架構。以我們的案例來說,我們必須稍微降低呼叫階層複雜度,並新增檢查,防止程式碼在輸入資料維持不變時執行。實作完成後,時間軸的預期結果如下:

請注意,迷你地圖的算繪執行作業會發生兩次,而不是一次。這是因為系統會為每個設定檔繪製兩個迷你地圖:一個用於面板頂端的總覽,另一個用於從記錄中選取目前顯示設定檔的下拉式選單 (這個選單中的每個項目都包含所選設定檔的總覽)。不過,這兩者內容完全相同,因此可重複使用。
由於這些小地圖都是在畫布上繪製的圖片,因此只要使用 drawImage
canvas 公用程式,然後執行一次程式碼,即可節省額外時間。經過這項努力,群組的持續時間從 2.4 秒縮短至 140 毫秒。
結論
套用所有這些修正 (以及其他一些較小的修正) 後,設定檔載入時間軸的變化如下所示:
變更前:

變更後:

改善後的載入時間為 2 秒,表示改善幅度約為 80%,而且由於大部分都是快速修正,因此相對輕鬆。當然,一開始正確找出該做什麼是關鍵,而效能面板就是合適的工具。
此外,請務必強調這些數字是針對做為研究對象的特定設定檔。這個設定檔特別大,因此我們很感興趣。不過,由於每個設定檔的處理管道都相同,因此這項重大改善適用於效能面板中載入的每個設定檔。
重點整理
從這些結果中,我們可學到一些有關應用程式效能最佳化的經驗:
1. 使用剖析工具找出執行階段效能模式
剖析工具非常實用,可協助您瞭解應用程式執行期間的動態,特別是找出提升效能的機會。Chrome 開發人員工具中的「效能」面板是網頁應用程式的絕佳選擇,因為這是瀏覽器中的原生網頁剖析工具,而且會持續維護,確保與最新的網頁平台功能相容。此外,現在速度也快上許多!😉
使用可做為代表性工作負載的樣本,看看能找到什麼!
2. 避免複雜的呼叫階層
盡可能避免讓呼叫圖過於複雜。如果呼叫階層複雜,很容易就會導致效能回歸,也很難瞭解程式碼的執行方式,因此難以改善。
3. 找出不必要的工作
程式碼庫老化時,通常會包含不再需要的程式碼。以我們的案例來說,舊版和不必要的程式碼佔據了總載入時間的很大一部分。移除這項功能是我們最容易做到的事。
4. 妥善使用資料結構
使用資料結構來提升成效,但也要瞭解每種資料結構在決定使用時帶來的成本和取捨。這不僅是資料結構本身的空間複雜度,也是適用作業的時間複雜度。
5. 快取結果,避免複雜或重複的作業重複執行
如果執行作業的成本很高,建議您儲存結果,以備下次需要時使用。如果作業會執行多次,即使每次的成本不高,也建議這麼做。
6. 延後非必要工作
如果不需要立即取得工作輸出內容,且工作執行作業會延長重要路徑,請考慮延後執行工作,在實際需要輸出內容時再延遲呼叫工作。
7. 對大型輸入內容使用有效率的演算法
對於大型輸入內容,最佳時間複雜度演算法至關重要。在這個範例中,我們並未深入探討這個類別,但其重要性不容小覷。
8. 加分題:為管道設定基準
為確保不斷演進的程式碼仍能快速運作,建議您監控程式碼行為並與標準比較。這樣一來,您就能主動找出回歸情形並提升整體可靠性,為長期成功奠定基礎。