實際的 React SPA 效能最佳化案例研究。
網站效能不僅取決於載入時間。為使用者提供快速且反應靈敏的體驗至關重要,尤其是使用者每天都會使用的生產力電腦應用程式。Recruit Technologies 的工程團隊進行重構專案,以改善其中一個網頁應用程式 AirSHIFT 的使用者輸入效能。以下是他們的做法。
回應速度緩慢,工作效率降低
AirSHIFT 是電腦版網頁應用程式,可協助餐廳和咖啡廳等商店業主管理員工輪班。採用 React 建構的單頁應用程式,提供豐富的用戶端功能,包括依日期、週次、月份等條件分類的班表表。
Recruit Technologies 工程團隊在 AirSHIFT 應用程式中新增功能後,開始收到更多有關效能緩慢的意見回饋。AirSHIFT 工程管理員 Yosuke Furukawa 表示:
在使用者研究研究中,我們發現有店家老闆表示,她會在按下按鈕後離開座位沖泡咖啡,只為了打發時間,等待班表表格載入。這讓我們感到震驚。
研究完成後,工程團隊發現許多使用者嘗試在低規電腦 (例如 10 年前的 1 GHz Celeron M 筆電) 上載大量的時間表。
AirSHIFT 應用程式用昂貴的指令碼阻斷主執行緒,但工程團隊並未意識到,由於這些指令碼是在快速 Wi-Fi 連線、功能豐富的規格電腦上開發及測試,所以指令碼成本高昂。
在啟用 CPU 和網路節流功能的 Chrome 開發人員工具中分析應用程式效能後,就會發現需要最佳化效能。AirSHIFT 促成瞭解決這個問題的工作人員。以下是他們著重於的 5 個重點,讓應用程式更能回應使用者的輸入內容。
1. 將大型資料表虛擬化
顯示位移表需要許多昂貴的步驟:建構虛擬 DOM,並根據員工人數和時段等比例呈現在螢幕上。舉例來說,如果餐廳有 50 名員工,且想查看他們的每月班表,則資料表會是 50 名員工乘以 30 天,因此需要算繪 1,500 個單元元件。這項作業的成本非常高,尤其是在低規格裝置上。實際情況更糟。研究人員發現,有 200 名員工的店家需要在單一月度表中使用約 6,000 個單元格元件。
為了降低這項作業的成本,AirSHIFT 將排班表虛擬化。應用程式現在只會掛載可視區域中的元件,並卸載畫面外元件。
在這個案例中,AirSHIFT 使用了 react-virtualized,因為系統需要啟用複雜的二維格線表格。他們也正在研究如何將實作內容轉換為日後可使用輕量型 react-window 的版本。
結果
單是將資料表虛擬化,就縮短了 6 秒的腳本時間 (在 CPU 減速 4 倍 + 限制 3G 速度的 MacBook Pro 環境中)。這是重構專案中效能提升最顯著的部分。
2. 使用 User Timing API 進行稽核
接著,AirSHIFT 團隊重構了根據使用者輸入內容執行的指令碼。Chrome 開發人員工具的火焰圖可用來分析主執行緒實際發生的情況。但 AirSHIFT 團隊發現,根據 React 的生命週期分析應用程式活動會比較簡單。
React 16 會透過 User Timing API 提供效能追蹤記錄,您可以在 Chrome 開發人員工具的「Timings」部分中查看這些記錄。AirSHIFT 使用「Timings」(時間) 部分,找出在 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 中執行的計算邏輯,並透過通訊公開
// 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 節流模擬)。
AirSHIFT 目前正在研究是否可以延遲載入其他元件,並將更多邏輯卸載至網路工作站,進一步減少卡頓情形。
4. 設定效能預算
實施所有這些最佳化措施後,請務必確保應用程式能持續保持效能。AirSHIFT 現在使用 bundlesize 來處理目前的 JavaScript 和 CSS 檔案大小。除了設定這些基本預算外,他們還建立了資訊主頁,以便顯示輪班表載入時間的各個百分位數,藉此檢查應用程式是否即使在非理想情況下也能正常運作。
- 系統現在會評估每個 Redux 事件的指令碼完成時間
- 效能資料會在 Elasticsearch 中收集
- 每個事件的第 10、25、50 和 75 個百分位數成效都會以 Kibana 呈現
AirSHIFT 現在會監控輪班表表格載入事件,確保 75 百分位數的使用者在 3 秒內完成載入。目前這項預算尚未強制執行,但他們正在考慮在超出預算時,透過 Elasticsearch 發送自動通知。
結果
從上方圖表可知,AirSHIFT 目前大多會在 75 百分位使用者中達到 3 秒預算,並且在 25 百分位使用者中於 1 秒內載入班表表格。透過擷取來自不同條件和裝置的 RUM 效能資料,AirSHIFT 現可檢查新功能版本是否確實影響應用程式的效能。
5. 效能黑客松
雖然所有這些效能最佳化努力都很重要且具影響力,但要讓工程和業務團隊將非功能性開發工作列為優先,並不總是容易。其中的挑戰之一,是無法預先規劃某些成效最佳化作業。需要實驗和不斷嘗試的心態。
AirSHIFT 目前正在進行為期一天的內部效能黑客松,讓工程師專注於效能相關工作。在這些黑客松中,他們會移除所有限制,並尊重工程師的創意,也就是說,任何有助於提升速度的實作方式都值得考慮。為了加快黑客鬆活動,AirSHIFT 將這群人分成多個小型團隊,並每個團隊互相競爭,找出在 Lighthouse 效能分數中改進的對象。團隊之間的競爭非常激烈!🔥
結果
他們認為黑客松活動的做法很實用。
- 您可以在黑客鬆活動期間實際嘗試多種方法,並使用 Lighthouse 進行評估,這樣就能輕鬆偵測效能瓶頸。
- 在黑客松結束後,要說服團隊優先處理哪些項目,以便正式發布,就會比較容易。
- 這也是宣導速度的重要性,每位參與者都能瞭解程式碼編寫方式與效能之間的關聯。
這項做法也意外獲得好評,許多 Recruit 內部的其他工程團隊都對這種實作方法感到興趣,AirSHIFT 團隊目前也在公司內舉辦多場快速黑客松。
摘要
對 AirSHIFT 來說,這些最佳化作業絕非輕鬆的旅程,但確實帶來了成果。現在,AirSHIFT 的排班表載入時間中位數為 1.5 秒,比專案前快上 6 倍。
效能最佳化功能啟動後,某位使用者表示:
非常感謝使位移資料表快速載入。 安排輪班工作現在變得更有效率。