運用績效感知功能,讓效能面板加快 400%

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

無論您開發的是哪種類型的應用程式,都必須盡可能提升效能,確保應用程式能快速載入並提供流暢的互動體驗,才能提供良好的使用者體驗,並確保應用程式成功。其中一種方法是使用剖析工具檢查應用程式的活動,瞭解應用程式在特定時間範圍內執行時的運作情形。開發人員工具中的 「效能」面板是一項出色的分析工具,可用於分析及改善網路應用程式的效能。如果應用程式在 Chrome 中執行,您就能透過視覺化方式,詳細掌握瀏覽器在執行應用程式時的運作情形。瞭解這類活動有助於找出模式、瓶頸和效能熱點,並依此採取行動來改善效能。

以下範例將逐步說明如何使用「Performance」面板。

設定及重建剖析情境

最近我們的目標是提高「效能」面板的效能。具體來說,我們希望它能更快載入大量成效資料。例如,當您要分析長時間執行或複雜的程序,或擷取高精細資料時,就會發生這種情況。為達成這項目標,您必須先瞭解應用程式「如何」執行,以及「為何」以這種方式執行,這可以透過使用分析工具來達成。

如您所知,開發人員工具本身是網頁應用程式。因此,您可以使用「效能」面板進行分析。如要分析這個面板本身,您可以開啟開發人員工具,然後開啟另一個附加的開發人員工具例項。在 Google 中,這種設定稱為「開發人員工具上的開發人員工具」

設定完成後,您必須重新建立並記錄要剖析的情況。為避免混淆,我們將原始的 DevTools 視窗稱為「第一個」DevTools 例項,而檢查第一個例項的視窗則稱為「第二個」DevTools 例項。

螢幕截圖:開發人員工具例項檢查開發人員工具本身的元素。
開發人員工具上的開發人員工具:使用開發人員工具檢查開發人員工具。

在第二個開發人員工具例項上,「效能」面板 (從這裡開始稱為「perf 面板」) 會觀察第一個開發人員工具例項,重新建立載入設定檔的情況。

第二個開發人員工具執行個體開始錄製,在第一個例項中,設定檔則會從磁碟上的檔案載入。系統會載入大型檔案,以便準確剖析處理大量輸入內容的效能。當兩個例項的載入作業完成後,效能分析資料 (通常稱為「追蹤」) 就會顯示在效能面板載入設定檔的第二個開發人員工具例項中。

初始狀態:找出改進空間

載入完成後,我們在第二個效能面板例項上觀察到下列內容,如下一個螢幕截圖所示。請專注於主要執行緒的活動,該活動會顯示在標示為「Main」的軌跡下方。從火焰圖中可以看到,活動分為五大類別。這些工作包括載入時間最長的任務。這些工作總共花費約 10 秒。在下方的螢幕截圖中,成效面板用來將焦點放在每個活動群組,以便找出可查看的內容。

螢幕截圖:開發人員工具中的效能面板,檢查另一個開發人員工具例項的效能追蹤畫面載入情形。載入設定檔大約需要 10 秒的時間。這段時間主要分為五大活動群組。

第一個活動群組:不必要的工作

很明顯,第一組活動是仍在執行的舊版程式碼,但實際上並不需要。基本上,綠色區塊下所有標示為 processThreadEvents 的項目都浪費了心力。這個問題很快就解決了。移除該函式呼叫後,可節省約 1.5 秒的時間。太棒了!

第二個活動群組

在第二個活動群組中,解決方案不像第一個群組那麼簡單。buildProfileCalls 耗時約 0.5 秒,且無法避免這項工作。

開發人員工具中檢查另一個效能面板執行個體的效能面板螢幕截圖。與 buildProfileCalls 函式相關聯的工作大約需要 0.5 秒。

出於好奇,我們在效能面板中啟用「Memory」選項,進一步進行調查,發現 buildProfileCalls 活動也使用大量記憶體。在這裡,您可以看到藍線圖在執行 buildProfileCalls 的時候突然跳動,這表示可能有記憶體流失的情況。

開發人員工具中記憶體分析器的螢幕截圖,用於評估效能面板的記憶體用量。檢查器指出,buildProfileCalls 函式會造成記憶體耗損。

為了確認這個懷疑,我們使用了「記憶體」面板 (DevTools 中的另一個面板,與效能面板中的「記憶體」抽屜不同) 進行調查。在「Memory」(記憶體) 面板中,選取「Allocation sampling」(分配取樣) 剖析類型,為載入 CPU 設定檔的效能面板記錄堆積快照。

記憶體分析器的初始狀態螢幕截圖。「分配取樣」選項以紅色方塊標示,表示這個選項最適合用於 JavaScript 記憶體分析。

下圖顯示所收集的堆積積木快照。

記憶體分析器的螢幕截圖,已選取,其中含有大量記憶體的集合作業。

從這個堆積快照中,我們發現 Set 類別會消耗大量記憶體。透過檢查呼叫點,我們發現我們對 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 毫秒。

變更前:

在對 appendEventAtLevel 函式進行最佳化前,成效面板的螢幕截圖。函式執行的總時間為 1,372.51 毫秒。

變更後:

對 appendEventAtLevel 函式進行最佳化後,效能面板的螢幕截圖。這個函式的總執行時間為 207.2 毫秒。

第四個活動群組:延後非必要工作並快取資料,以免重複執行工作

放大這個視窗,您可以看到有兩個幾乎相同的函式呼叫區塊。查看呼叫的函式名稱,您可以推斷這些區塊包含建立樹狀結構的程式碼 (例如 refreshTreebuildChildren 等名稱)。事實上,相關程式碼就是在面板底部抽屜中建立樹狀檢視畫面的程式碼。值得一提的是,這些樹狀檢視在載入後並不會立即顯示。相反地,使用者必須選取樹狀檢視畫面 (抽屜中的「自下而上」、「呼叫樹狀圖」和「事件記錄」分頁),才能顯示樹狀圖。此外,如您從螢幕截圖中看到的,樹狀結構建構程序已執行兩次。

效能面板的螢幕截圖,顯示多個重複執行的工作,即使不需要的情況也是如此。這些工作可延後執行,以便視需要執行,而非提前執行。

下圖有兩個問題:

  1. 非關鍵工作會拖慢載入時間效能。使用者不一定需要輸出內容。因此,此工作對設定檔載入作業而言並非必要。
  2. 系統未快取這些工作的結果。因此,即使資料沒有變更,樹狀圖仍會計算兩次。

我們一開始會將樹狀圖計算作業延後至使用者手動開啟樹狀圖檢視畫面時執行。只有在這種情況下,才值得花錢建立這些樹木。執行這項作業兩次的總時間約為 3.4 秒,因此延後執行作業可大幅縮短載入時間。我們仍在考慮快取這類工作。

第五個活動群組:請盡量避免使用複雜的呼叫階層

仔細查看這個群組後,我們發現系統不斷呼叫特定呼叫鏈結。同樣的模式在火焰圖的不同位置出現了 6 次,這個時間窗口的總時間長度約為 2.4 秒!

效能面板的螢幕截圖,顯示六個個別函式呼叫,用於產生相同的追蹤迷你地圖,每個函式呼叫都包含深層呼叫堆疊。

多次呼叫的相關程式碼是處理要在「迷你地圖」(面板頂端的時間軸活動概覽) 上算繪的資料的部分。我們不清楚為何會發生多次,但肯定不是 6 次!事實上,如果沒有載入其他設定檔,程式碼的輸出內容應保持不變。理論上,程式碼應只執行一次。

經過調查後,我們發現系統會呼叫相關程式碼,因為載入管道中的多個部分會直接或間接呼叫用於計算迷你地圖的函式。這是因為程式的呼叫圖表複雜度會隨著時間演進,且使用者可能不知不覺地新增了更多此程式碼的依附元件。這個問題沒有快速解決方法。解決方式取決於問題中程式碼集的架構。在我們的案例中,我們必須稍微降低呼叫階層的複雜度,並新增檢查項目,以免在輸入資料未變更的情況下執行程式碼。實作這項功能後,我們得到了這個時間軸:

效能面板的螢幕截圖,顯示產生相同追蹤迷你地圖的六個個別函式呼叫,已減少至兩次。

請注意,小導覽圖算繪作業會執行兩次,而不是一次。這是因為每個設定檔都會繪製兩個迷你地圖:一個用於面板頂端的總覽,另一個用於下拉式選單,可從記錄中選取目前顯示的設定檔 (這個選單中的每個項目都包含所選設定檔的總覽)。不過,這兩個檔案的內容完全相同,因此可以重複使用。

由於這些迷你地圖都是在畫布上繪製的圖片,因此只要使用 drawImage canvas 公用程式,然後只執行一次程式碼即可,這樣就能節省一些時間。這項努力的結果是,群組的時間長度從 2.4 秒縮短為 140 毫秒。

結論

套用所有修正項目 (以及其他一些較小的修正項目) 後,設定檔載入時間軸的變化如下:

變更前:

效能面板的螢幕截圖,顯示最佳化前載入追蹤記錄。這項程序大約需要十秒鐘。

變更後:

效能面板的螢幕截圖,顯示最佳化後的追蹤載入情形。這項程序現在大約需要兩秒鐘。

改善後的載入時間為 2 秒,也就是說,改善幅度約為 80%,且所需的努力相對較少,因為大部分的改善措施都是快速修正。當然,正確辨別一開始「應該做」的是關鍵,而效能樣本就是絕佳的利器。

請注意,這些數字僅適用於用於研究的個人資料。這個檔案格外龐大,因此我們對這項設定檔特別感興趣。不過,由於每個設定檔的處理管道都是相同的,因此能大幅改善的改善項目將套用至效能面板內載入的每個設定檔。

重點整理

從這些結果中,我們可以學到一些關於應用程式效能最佳化的課題:

1. 善用剖析工具找出執行階段效能模式

如要瞭解應用程式運作期間的情況,就特別適合使用剖析工具,尤其是找出能改善效能的機會。Chrome 開發人員工具中的「效能」面板是網路應用程式的絕佳選項,因為這是瀏覽器中的原生網路分析工具,且會積極維護,以便隨時更新最新的網路平台功能。此外,現在的速度也大幅提升!😉

使用可做為代表性工作負載的範例,看看您能發現什麼!

2. 避免複雜的呼叫階層

盡量避免讓呼叫圖表過於複雜。使用複雜的呼叫階層很容易導致效能倒退,而且很難瞭解程式碼為何以這種方式執行,因此很難進行改善。

3. 找出不必要的工作

老舊程式碼集包含不再需要的程式碼。在我們的案例中,舊版和不必要的程式碼佔了總載入時間的大部分。移除這項功能是垂手可得的收益。

4. 適當使用資料結構

使用資料結構來提升效能,但在決定要使用哪種資料結構時,也要瞭解每種資料結構帶來的成本和取捨。這不僅是資料結構本身的空間複雜度,也是適用作業的時間複雜度。

5. 快取結果,避免複雜或重複的作業重複執行

如果執行作業的成本高昂,建議您儲存結果,以便在下次需要時使用。如果作業執行多次,這麼做也相當合理,即使每次執行作業的成本不高也是如此。

6. 延後非必要工作

如果不需要立即取得工作輸出內容,且工作執行作業會延長關鍵路徑,建議您在實際需要輸出內容時,以延遲方式呼叫工作。

7. 在大量輸入內容上使用高效演算法

對於大量輸入內容,最佳的時間複雜度演算法就顯得至關重要。我們在這個範例中並未探討這個類別,但這類內容的重要性不容小覷。

8. 額外步驟:對管道進行基準測試

為確保不斷演進的程式碼仍能維持快速,建議您監控行為並與標準進行比較。這樣一來,您就能主動找出回歸情形並改善整體可靠性,為長期成功奠定基礎。