AirSHIFT 改善 React 應用程式' 執行階段效能的五種方法

實際的 React SPA 效能最佳化案例研究。

Kento Tsuji
Kento Tsuji
Satoshi Arai
Satoshi Arai
Yusuke Utsunomiya
Yusuke Utsunomiya
Yosuke Furukawa
Yosuke Furukawa

網站效能不僅取決於載入時間。為使用者提供快速且反應靈敏的體驗至關重要,尤其是使用者每天都會使用的生產力電腦應用程式。Recruit Technologies 的工程團隊進行重構專案,以改善其中一個網頁應用程式 AirSHIFT 的使用者輸入效能。以下是他們的做法。

回應速度緩慢,工作效率降低

AirSHIFT 是電腦版網頁應用程式,可協助餐廳和咖啡廳等商店業主管理員工輪班。這個單頁應用程式採用 React 建構,提供豐富的用戶端功能,包括以日、週、月等分類的各種輪班表格格狀表格。

AirSHIFT 網頁應用程式的螢幕截圖。

Recruit Technologies 工程團隊在 AirSHIFT 應用程式中新增功能後,開始收到更多有關效能緩慢的意見回饋。AirSHIFT 工程管理員 Yosuke Furukawa 表示:

在使用者研究中,我們發現有店家老闆表示,她會在按下按鈕後離開座位沖泡咖啡,只為了打發時間,等待班表表格載入。這讓我們感到相當震驚。

研究完成後,工程團隊發現許多使用者試圖在低規電腦 (例如 10 年前的 1 GHz Celeron M 筆電) 上載大量的時間表。

低階裝置上的無限旋轉圖示。

AirSHIFT 應用程式會使用耗用大量資源的指令碼阻斷主執行緒,但工程團隊並未意識到指令碼的耗用資源量,因為他們是在具備快速 Wi-Fi 連線的豐富規格電腦上進行開發和測試。

顯示應用程式執行階段活動的圖表。
載入輪班表時,執行指令碼會耗用約 80% 的載入時間。

在 Chrome 開發人員工具中啟用 CPU 和網路節流功能,並剖析效能後,他們發現需要進行效能最佳化調整。AirSHIFT 已成立專案小組來解決這個問題。以下是他們專注於讓應用程式更能回應使用者輸入內容的 5 個重點。

1. 將大型資料表虛擬化

要顯示班表,需要執行多個耗時的步驟:建構虛擬 DOM,並根據員工人數和時間間隔在畫面上算繪。舉例來說,如果餐廳有 50 名員工,且想查看他們的每月班表,則資料表會是 50 名員工乘以 30 天,因此需要算繪 1,500 個單元元件。這項作業的成本非常高,尤其是在低規格裝置上。實際上,情況更糟。研究人員發現,有 200 名員工的店家需要在單一月度表中使用約 6,000 個單元格元件。

為了降低這項作業的成本,AirSHIFT 將輪班表轉為虛擬化。應用程式現在只會掛載可視區域中的元件,並卸載畫面外元件。

附註螢幕截圖,說明 AirSHIFT 用於在檢視區外算繪內容。
之前:算繪所有時段表格儲存格。
附註螢幕截圖,說明 AirSHIFT 現在只會轉譯可視區域中可見的內容。
改變後:只會在可視區域中顯示儲存格。

在這個案例中,AirSHIFT 使用了 react-virtualized,因為系統需要啟用複雜的二維格線表格。他們也正在研究如何將實作項目轉換為日後可使用輕量型 react-window 的項目。

結果

單是將資料表虛擬化,就縮短了 6 秒的腳本時間 (在 CPU 減速 4 倍 + 限制 3G 速度的 MacBook Pro 環境中)。這是重構專案中效能提升最顯著的部分。

註解說明的 Chrome 開發人員工具「效能」面板錄製畫面。
前:使用者輸入後約 10 秒的腳本。
另一張註解螢幕截圖,顯示 Chrome 開發人員工具「效能」面板的錄製畫面。
後置:使用者輸入後 4 秒的腳本。

2. 使用 User Timing API 進行稽核

接著,AirSHIFT 團隊重構了根據使用者輸入內容執行的指令碼。Chrome 開發人員工具火焰圖可用來分析主執行緒實際發生的情況。不過,AirSHIFT 團隊發現,根據 React 的生命週期分析應用程式活動更容易。

React 16 會透過 User Timing API 提供效能追蹤記錄,您可以透過 Chrome 開發人員工具的「Timings」部分查看這些記錄。AirSHIFT 使用「Timings」(時間) 部分,找出在 React 生命週期事件中執行不必要的邏輯。

Chrome 開發人員工具「效能」面板的「時間」部分。
React 的使用者時間事件。

結果

AirSHIFT 團隊發現,在每次路徑導覽前,系統會進行不必要的 React 樹狀圖和解讀作業。這表示 React 在導覽前不必要更新輪班表。這個問題是因為不必要的 Redux 狀態更新所造成。修正後,指令碼時間可節省約 750 毫秒。AirSHIFT 也進行了其他微型最佳化,最終使指令碼執行時間總共縮短 1 秒。

3. 延遲載入元件,並將耗用大量資源的邏輯移至網路工作者

AirSHIFT 有內建的即時通訊應用程式。許多商店老闆會在查看班表時透過即時通訊與員工溝通,這表示使用者可能會在表格載入時輸入訊息。如果主執行緒被用於繪製表格的指令碼佔用,使用者輸入可能會卡頓。

為改善這項體驗,AirSHIFT 現在使用 React.lazy 和 Suspense,在延遲載入實際元件時,顯示表格內容的預留位置。

AirSHIFT 團隊也將部分在延遲載入元件中耗用大量資源的業務邏輯,遷移至網路工作站。這麼做可釋放主執行緒,讓它專注於回應使用者輸入內容,進而解決使用者輸入卡頓問題。

開發人員通常會面臨使用 worker 的複雜性,但這次 Comlink 為他們完成繁重的工作。以下是 AirSHIFT 將最耗時的作業之一 (計算總勞動成本) 自動化處理的偽程式碼。

在 App.js 中,使用 React.lazy 和 Suspense 在載入期間顯示備用內容

/** App.js */
import React, { lazy, Suspense } from 'react'

// Lazily loading the Cost component with React.lazy
const Hello = lazy(() => import('./Cost'))

const Loading = () => (
 
<div>Some fallback content to show while loading</div>
)

/
/ Showing the fallback content while loading the Cost component by Suspense
export default function App({ userInfo }) {
   
return (
   
<div>
     
<Suspense fallback={<Loading />}>
       
<Cost />
     
</Suspense>
    </
div>
 
)
}

在「Cost」元件中,使用 comlink 執行計算邏輯

/** Cost.js */
import React from 'react';
import { proxy } from 'comlink';

// import the workerlized calc function with comlink
const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
export default async function Cost({ userInfo }) {
 
// execute the calculation in the worker
 
const instance = await new WorkerlizedCostCalc();
 
const cost = await instance.calc(userInfo);
 
return <p>{cost}</p>;
}

實作在 worker 中執行的計算邏輯,並透過 comlink 公開

// WorkerlizedCostCalc.js
import { expose } from 'comlink'
import { someExpensiveCalculation } from './CostCalc.js'

// Expose the new workerlized calc function with comlink
expose
({
  calc
(userInfo) {
   
// run existing (expensive) function in the worker
   
return someExpensiveCalculation(userInfo);
 
}
}, self);

結果

雖然 AirSHIFT 在測試期間只將部分邏輯轉為 worker,但他們將約 100 毫秒的 JavaScript 從主執行緒轉移至 worker 執行緒 (模擬使用 4 倍 CPU 節流)。

Chrome 開發人員工具「效能」面板錄製畫面,顯示目前是在網路 worker 而非主執行緒上執行指令碼。

AirSHIFT 目前正在研究是否可以延遲載入其他元件,並將更多邏輯卸載至網路工作站,進一步減少卡頓情形。

4. 設定效能預算

實施所有這些最佳化措施後,請務必確保應用程式能持續保持效能。AirSHIFT 現在會使用 bundlesize,確保不會超過目前 JavaScript 和 CSS 檔案大小。除了設定這些基本預算外,他們還建立了資訊主頁,顯示輪班表載入時間的各個百分位數,以便檢查應用程式是否即使在非理想情況下也能正常運作。

  • 系統現在會評估每個 Redux 事件的指令碼完成時間
  • Elasticsearch 會收集成效資料
  • 使用 Kibana 將各事件的第 10、25、50 和 75 個百分位數表現以圖表呈現

AirSHIFT 現在會監控輪班表表格載入事件,確保 75 百分位數的使用者在 3 秒內完成載入。目前這項預算尚未強制執行,但他們正在考慮在超出預算時,透過 Elasticsearch 發送自動通知。

圖表顯示第 75 個百分位數的完成時間約為 2500 毫秒、第 50 個百分位數的完成時間約為 1250 毫秒、第 25 個百分位數的完成時間約為 750 毫秒,而第 10 個百分位數的完成時間約為 500 毫秒。
Kibana 資訊主頁會依百分位數顯示每日成效資料。

結果

從上方圖表可知,AirSHIFT 目前大多會在 75 百分位使用者中達到 3 秒預算,並且在 25 百分位使用者中於 1 秒內載入班表表格。透過擷取來自不同條件和裝置的 RUM 效能資料,AirSHIFT 現可檢查新功能版本是否確實影響應用程式的效能。

5. 效能黑客松

雖然所有這些效能最佳化措施都很重要且具影響力,但要讓工程和業務團隊將非功能性開發工作列為優先,並不總是容易。其中的挑戰之一,是無法預先規劃某些成效最佳化作業。需要實驗和嘗試錯誤的思維。

AirSHIFT 目前正在進行為期一天的內部效能黑客松,讓工程師專注於效能相關工作。在這些黑客松中,他們會移除所有限制,並尊重工程師的創意,也就是說,任何有助於提升速度的實作方式都值得考慮。為了加快黑客松的進度,AirSHIFT 將團隊分成小組,各組競賽,看看誰能獲得最大的 Lighthouse 成效分數提升幅度。團隊之間的競爭非常激烈!🔥?

黑客松活動的相片。

結果

他們認為黑客松活動的做法很實用。

  • 在黑客松期間實際嘗試多種方法,並使用 Lighthouse 評估每種方法,即可輕鬆偵測效能瓶頸。
  • 在黑客松結束後,要說服團隊優先處理哪些項目,以便正式發布,就會比較容易。
  • 這也是宣導速度的重要性,每位參與者都能瞭解程式碼編寫方式與效能之間的關聯。

這項做法也意外獲得好評,許多 Recruit 內部的其他工程團隊都對這種實作方法感到興趣,AirSHIFT 團隊目前也在公司內舉辦多場快速黑客松。

摘要

對 AirSHIFT 來說,這些最佳化作業絕非輕鬆的旅程,但確實帶來了成果。現在,AirSHIFT 的排班表載入時間中位數為 1.5 秒,比專案前快上 6 倍。

效能最佳化功能推出後,一位使用者表示:

非常感謝你讓班表載入速度加快。 安排輪班工作現在變得更有效率。