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

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

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

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

設定及重現剖析情境

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

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

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

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

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

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

初始狀態:找出改善機會

載入完成後,我們在第二個效能面板例項上觀察到下列內容,如下一個螢幕截圖所示。請專注於主要執行緒的活動,該活動會顯示在標示為「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 變更為一般陣列。套用這項變更後,系統會拍攝另一個堆積快照,並觀察減少的記憶體配置。雖然這項變更並未大幅提升速度,但帶來的次要效益是應用程式發生當機的頻率降低了。

記憶體分析器的螢幕截圖。先前耗用大量記憶體的 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. 額外獎勵:管道基準

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