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

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

無論開發哪種應用程式,最佳化其效能、確保應用程式能快速載入,並提供順暢的互動功能,對使用者體驗和應用程式成效都至關重要。要確認應用程式的活動情況,其中一種方法是使用剖析工具檢查應用程式的活動,瞭解某一段時間內執行時的實際運作情形。開發人員工具中的「效能」面板是絕佳的剖析工具,可用來分析網頁應用程式並最佳化效能。如果您的應用程式在 Chrome 中執行,可以清楚顯示瀏覽器在執行應用程式時正在執行的操作。瞭解該活動有助您找出模式、瓶頸和效能熱點,並據此採取行動來改善效能。

以下範例將逐步引導您使用「Performance」面板。

設定並重新建立剖析情境

近期,我們設定了一些目標,讓「成效」面板的效能提升。尤其希望能更快載入大量成效資料。舉例來說,剖析長時間執行或複雜的程序,或是擷取高精細度資料時,就會發生這種情況。為此,您需要瞭解應用程式執行的「方式」,以及應用程式一開始必須以這種方式執行的「原因」(透過剖析工具達成)。

如您所知,開發人員工具本身是網頁應用程式。因此,您可以使用「效能」面板剖析資料。如要剖析這個面板本身,可以開啟開發人員工具,然後開啟另一個與這個面板連結的 DevTools 執行個體。在 Google,這項設定稱為開發人員工具 (開發人員工具)

設定完成後,必須重新建立並記錄情境。為避免造成混淆,原本的開發人員工具視窗將稱為「第一個」開發人員工具視窗,用來檢查第一個例項的視窗稱為「第二個」開發人員工具執行個體。

開發人員工具執行個體正在檢查開發人員工具本身的元素。
DevTools-on-DevTools:使用開發人員工具檢查開發人員工具。

在第二個開發人員工具執行個體中,從這裡開始稱為「perf 面板」的「Performance」面板,會觀察第一個 DevTools 執行個體重新建立情境,並載入設定檔。

第二個開發人員工具執行個體中,啟動即時錄製內容時,在第一個執行個體中,系統會從磁碟上的檔案載入設定檔。系統會載入大型檔案,以便準確分析處理大型輸入內容的效能。兩個執行個體都載入完畢後,效能剖析資料 (通常稱為「追蹤記錄」trace) 會顯示在 Perf 面板載入設定檔的第二個 DevTools 執行個體中。

初始狀態:找出改善機會

載入完畢後,我們在下一張螢幕截圖中發現第二個 Perf 面板例項會顯示以下內容。將焦點放在主要執行緒的活動,該活動會顯示在標示為「Main」的測試群組下方。火焰圖中可看到五個大型活動群組。這些工作包括載入時間最長的工作。這些工作的總時間長度約為 10 秒。在下方螢幕截圖中,我們使用成效面板將重點放在每個活動群組,以便查看相關資訊。

開發人員工具中的效能面板螢幕截圖,顯示其他開發人員工具執行個體的效能追蹤記錄載入情形。設定檔的載入時間大約需要 10 秒。這個時間主要分成五大活動群組。

第一個活動群組:必要作業

第一組活動顯然是仍在執行的舊程式碼,但實際上並不需要。基本上,在標示為 processThreadEvents 的綠色區塊底下,一切都是原本浪費的資源。剛剛的表現相當出色。移除該函式呼叫約可省下 1.5 秒。太棒了!

第二個活動群組

在第二個活動群組中,解決方案並不像第一個活動那麼簡單。buildProfileCalls 花費約 0.5 秒,但該工作無法避免。

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

為了引起他們的好奇心,我們在 Perf 面板中啟用「Memory」選項,以便進一步調查,並發現 buildProfileCalls 活動也使用大量記憶體。在這裡,您可以看到藍色折線圖在執行 buildProfileCalls 時突然跳出,表示潛在的記憶體流失。

開發人員工具中的記憶體分析器「評估效能面板的記憶體用量」。檢查器建議 buildProfileCalls 函式負責發生記憶體流失。

為了進一步探討這個疑慮,我們使用「Memory」面板 (開發人員工具中的另一個面板,與 Perf 面板中的記憶體導覽匣不同) 進行調查。在「Memory」面板中選取「Allocation sampling」剖析類型,系統會記錄載入 CPU 設定檔的 Perf 面板堆積快照。

記憶體分析器初始狀態的螢幕截圖。「分配取樣」選項會以紅色方塊醒目顯示,表示此選項最適合用來進行 JavaScript 記憶體剖析。

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

記憶體分析器的螢幕截圖,其中已選取會耗用大量記憶體的 Set 式作業。

從這個堆積快照中,Set 類別會耗用大量記憶體。藉由檢查呼叫點,我們發現對大量建立的物件而言,Set 類型的屬性是不必要的指派。到目前為止,這項成本增加、耗用大量記憶體,達到了應用程式大規模輸入裝置異常終止的常見原因。

組合適合儲存不重複項目,並提供使用內容獨特性的操作,例如複製資料集,提高查詢效率。不過,由於儲存的資料不會與來源內容重複,因此這些功能並非必要。因此,一開始就沒有必要組合。為改善記憶體配置,已將屬性類型從 Set 變更為純陣列。套用這項變更後,系統又拍攝了另一個堆積快照,並觀察到縮減記憶體配置。雖然透過本次變更無法大幅提升速度,但另一個好處是應用程式當機的頻率較低。

記憶體分析器的螢幕截圖。先前會耗用大量記憶體的 Set 作業已變更為使用純陣列,這會大幅降低記憶體成本。

第三個活動群組:權衡資料結構的取捨

第三部分是特殊的:您可以在火焰圖中看到,其中包含窄但高的欄,代表深層函式呼叫,在本例中是深遞迴。總共有大約 1.4 秒。從本節底部可看出,這些欄的寬度是取決於一項函式的時間長度:appendEventAtLevel。這可能表示該欄可能是瓶頸

appendEventAtLevel 函式的實作中,我們有一件事可以理解。對於輸入中的每個資料項目 (在程式碼中稱為「事件」),系統會將項目加進地圖,追蹤時間軸項目的垂直位置。這會造成問題,因為儲存的項目數量非常龐大。「Google 地圖」雖然速度較快,但透過鍵查詢查詢,但這個優點並不容易。隨著地圖日益擴張,加入資料可以增加費用,例如,重新雜湊處理會導致地圖費用增加。當連續新增大量項目至地圖時,這筆費用就會變得明顯。

/**
 * 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 毫秒間的位移時間。

變更前:

將最佳化前的效能面板的螢幕截圖,顯示在 endEventAtLevel 函式中。函式的執行總時間是 1,372.51 毫秒。

變更後:

對 AttachEventAtLevel 函式最佳化調整後的效能面板螢幕截圖。函式的執行總時間是 207.2 毫秒。

第四項活動群組:延遲非關鍵工作並快取資料,以防止重複作業

放大這個視窗,可以發現函式呼叫有兩個幾乎相同的區塊。透過查看呼叫函式的名稱,您可以推論這些區塊由建構樹狀結構的程式碼 (例如名稱如 refreshTreebuildChildren 等名稱) 組成。事實上,相關的程式碼就是在面板底部導覽匣中建立樹狀結構檢視畫面的程式碼。值得注意的是,這些樹狀檢視在載入後並未立即顯示。相反地,使用者需要選取樹狀檢視 (導覽匣中的「Bottom-up」、「Call Tree」和「Event Log」分頁),才能顯示樹狀結構。此外,如螢幕截圖所示,系統已執行兩次樹狀結構建構程序。

效能面板的螢幕截圖,顯示多項重複執行的工作,即使不需要這些工作也能執行。這些工作可以按照需求延後執行,而不是提前執行。

我們在這張圖中發現兩個問題:

  1. 非重要工作會拖慢載入時間的效能。使用者不一定需要使用輸出內容。因此,該工作對於設定檔載入並不重要。
  2. 未快取這些工作的結果。因此,儘管資料沒有變化,這些樹木也計算了兩次。

我們已將樹狀結構計算結果延後至使用者手動開啟樹狀檢視時。只須支付建立這些樹種的費用。執行此兩次的總時間約為 3.4 秒,因此延後這個時間可大幅縮短載入時間。我們也在研究快取這些類型的工作。

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

仔細查看此群組,顯然已經重複叫用特定呼叫鏈。相同的圖案在火焰圖中的不同位置出現 6 次,而這個視窗的總時間長度約為 2.4 秒!

效能面板的螢幕截圖,顯示六個分別用於產生相同追蹤記錄小圖的函式呼叫,每個函式呼叫都有深度呼叫堆疊。

多次呼叫相關程式碼是處理資料以顯示在「小導覽圖」 (面板頂端的時間軸活動總覽) 的部分。你可能很不清楚為何會發生這個問題,但事實上,這也沒辦法發生 6 次!事實上,在未載入其他設定檔的情況下,程式碼的輸出內容應維持目前狀態。理論上,程式碼只應執行一次。

經過調查後,我們發現相關的程式碼是直接或間接呼叫計算迷你映射的函式所導致的載入管道中多個部分的結果。這是因為程式呼叫圖的複雜性會隨著時間演進,而這程式碼在不知情的情況下加入了更多依附元件。這個問題目前沒有快速修正的方法。解決方法取決於相關程式碼集的架構。在本例中,我們必須稍微簡化呼叫階層,並加入檢查,避免在輸入資料維持不變的情況下執行程式碼。完成導入後,我們得知時間軸的大致如下:

效能面板的螢幕截圖,顯示產生相同追蹤記錄小導覽的六個不同函式呼叫,減少為只有兩次。

請注意,小導覽圖轉譯作業會執行兩次,而非一次。這是因為每個個人資料都會繪製兩個迷你地圖:一個用於在面板上方總覽,另一個用於下拉式選單,讓使用者從記錄中選取目前可見的個人資料 (這個選單的所有項目都包含選取的設定檔總覽)。儘管如此,兩者的內容完全相同,因此可以重複使用。

這些小地圖都是在畫布上繪製的圖片,因此我們只要使用 drawImage 畫佈公用程式,然後只執行一次程式碼,就能節省更多時間。因此,該群組的時間從 2.4 秒縮減為 140 毫秒。

結論

套用所有修正項目 (以及這裡和其中幾個較小的修正項目) 之後,設定檔載入時間軸的變更情形如下:

變更前:

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

變更後:

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

改善後的載入時間只有 2 秒。也就是說,由於大部分程式碼都經過快速修正,所以改善幅度約為 80%。當然,一開始正確找出「目標」是關鍵,而效能面板就是適合這項工具的工具。

另外,請務必強調這些數據,特別指出這些是某個個人資料用於研究的主題。個人資料的大小特別龐大,因此對我們感興趣。不過,由於每個設定檔的處理管道都相同,因此大幅提升的改善成果會套用至「效能」面板中載入的每個設定檔。

重點摘要

就您的應用程式效能最佳化而言,還有一些經驗教訓:

1. 使用剖析工具識別執行階段效能模式

瞭解應用程式執行期間的情況,特別適合用來找出提升成效的機會。Chrome 開發人員工具的「效能」面板是網頁應用程式的理想選擇,因為這是瀏覽器內建的網頁剖析工具,並且會主動維護,即時支援最新的網路平台功能。而且,其速度顯著提升!😉

使用可以做為代表工作負載的範例,看看能找到什麼!

2. 避免複雜的呼叫階層

可以的話,請避免讓呼叫圖變得過於複雜。如果呼叫階層很複雜,就很容易造成效能迴歸,也難以瞭解程式碼為何執行程式碼的運作方式,導致難以改進土地。

3. 找出不必要的作業

程式碼集包含不再需要的程式碼是很常見的情況。在本例中,舊版和不必要的程式碼佔總載入時間的大部分比例。把它移除是最普通的水果。

4. 適當使用資料結構

使用資料結構來最佳化效能,但也瞭解在決定使用何種資料結構時,會產生哪些成本和權衡取捨。這不僅僅是資料結構本身的空間複雜,適用作業也往往相當複雜。

5. 快取結果,避免針對複雜或重複作業的重複工作

如果執行的作業成本高昂,最好在下次需要時儲存結果。此外,如果作業很多次,即使每次作業成本都不太高,也很合理。

6. 延遲非重要工作

如果不需要任務的輸出內容就能立即執行,而工作執行作業延伸了關鍵路徑,請考慮在實際需要輸出內容時延遲呼叫,藉此延遲該路徑。

7. 對大量輸入使用有效的演算法

對於大型資料輸入內容,最佳時間複雜度演算法至關重要。這個範例沒有研究這個類別,但這項指標的重要性不高。

8. 額外好處:為管道設定基準

如要確保不斷演進的程式碼能夠持續運作,請務必監控行為,並與標準比較。如此一來,您可以主動找出迴歸並改善整體可靠性,為長期成功奠定基礎。