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 連線的豐富規格電腦上進行開發與測試。

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

在啟用 CPU 和網路節流的情況下,在 Chrome 開發人員工具中剖析效能後,就會發現需要效能最佳化。AirSHIFT 則成立了負責解決這個問題的任務團隊。以下介紹 5 大重點,希望能讓應用程式更回應使用者的輸入內容。

1. 將大型資料表虛擬化

顯示排班表需要多個昂貴的步驟:建構虛擬 DOM,並依照員工人數和時段的數量成比例在螢幕上呈現。舉例來說,假設某間餐廳有 50 名員工,並想查看每月班表,就會有一個表格是 50 (成員) 乘以 30 (天) 後,應該顯示 1,500 個儲存格元件。這是成本非常高的運算作業,在低規格的裝置上更是如此。實際上,情況惡化。根據研究結果,他們有商店管理 200 名員工,單個月的桌子需要約 6,000 個元件。

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

加註的螢幕截圖,展示 AirSHIFT 用來在可視區域外顯示內容。
調整前:算繪所有位移表格儲存格。
加上註解的螢幕截圖,證明 AirSHIFT 現在只會顯示可視區域中顯示的內容。
之後:只顯示可視區域中的儲存格。

在本例中,AirSHIFT 就使用了「反應虛擬化」,因為必須滿足啟用複雜二維格線表格的需求。他們也同時探索如何轉換實作方式,在未來使用輕量回應視窗

結果

光是將這個資料表虛擬化,指令碼時間就會縮短 6 秒 (4 倍 CPU 減速 + 快速 3G 限制的 Macbook Pro 環境)。這是重構專案中最具影響力的效能改善項目。

Chrome 開發人員工具效能面板錄製內容的加註解螢幕截圖。
開始前:在使用者輸入內容後,執行約 10 秒的指令碼。
Chrome 開發人員工具「效能」面板錄製內容的另一個加註解螢幕截圖。
之後:在使用者輸入內容後執行 4 秒指令碼。

2. 使用 User Timing API 進行稽核

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

React 16 透過 User Timing API 提供效能追蹤記錄,您可以在 Chrome 開發人員工具的時間部分以視覺化方式查看。AirSHIFT 使用「時間」區段找出在 React 生命週期事件中執行的非必要邏輯。

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

結果

AirSHIFT 團隊發現在每次路線導航之前,都發生不必要的回應樹狀結構協調。這表示 React 是在導覽開始前,無謂更新 Shift 資料表。這個問題導致發生不必要的 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 仍將 JavaScript 的 JavaScript 從主執行緒轉移到背景工作執行緒 (模擬 4 倍 CPU 限制)。

Chrome 開發人員工具「效能」面板錄製內容的螢幕截圖,說明指令碼現在是在網路工作站執行,而非主要執行緒。

AirSHIFT 目前正在探索是否能延遲載入其他元件,並向網路工作站卸載更多邏輯,以進一步減少卡頓。

4. 設定效能預算

完成上述所有最佳化作業後,就必須確保應用程式長時間維持良好效能。AirSHIFT 現在使用 bundlesize 不會超過目前的 JavaScript 和 CSS 檔案大小。除了設定這些基本預算之外,他們還建立了一個資訊主頁來顯示排班表載入時間的不同百分位數,以檢查應用程式即使在不理想的情況下也能效能良好。

  • 現在可以評估每個 Redux 事件的指令碼完成時間
  • Elasticsearch 中收集成效資料
  • 你可以透過 Kibana 以視覺化的方式瞭解各事件的第 10、25、50 和第 75 個百分位數的成效

AirSHIFT 現在會監控位移資料表載入事件,以確保在第 75 個百分位數的使用者可在 3 秒內完成這項作業。這是目前未強制執行的預算,但他們考慮在超出預算時,考慮透過 Elasticsearch 自動傳送通知。

圖表顯示完成的第 75 個百分位數在 2500 毫秒左右完成,第 50 個百分位數在 1250 毫秒內完成,第 25 個百分位數在 750 毫秒內完成,在 500 毫秒左右的第 10 個百分位數。
Kibana 資訊主頁,按百分位數顯示每日成效資料。

結果

從上圖中,您可以得知 AirSHIFT 現在幾乎將預算分配給第 75 個百分位數的使用者 3 秒,同時也在一秒內載入轉變表 (第 25 個百分位數)。AirSHIFT 現在可以從各種狀況和裝置擷取 RUM 效能資料,檢查新版本是否確實影響應用程式效能。

5. 表演黑客松

即使這些效能最佳化工作很重要且具影響力,但要讓工程和業務團隊將非功能性開發列為優先考量,並非易事。其中一項挑戰是無法規劃某些效能最佳化作業。他們需要不斷嘗試,並思考不同的嘗試和錯誤思維。

AirSHIFT 現在正在進行內部為期 1 天的效能駭客活動,讓工程師只專注處理與效能相關的工作。在這些黑客松中,他們消除了所有限制,並尊重工程師的創造力,因此任何會提升速度的實作都值得一試。為了加快黑客松,AirSHIFT 將小組分為多個小型團隊,每個團隊會彼此競爭,看看誰能提高 Lighthouse 效能分數的最大改善。團隊的競爭非常激烈!🔥

黑客鬆的相片。

結果

黑客鬆的做法很好。

  • 可以實際嘗試在黑客鬆活動中嘗試多種方法,並使用 Lighthouse 測量每個方法,很容易就能偵測到效能瓶頸。
  • 黑客鬆活動結束後,很簡單就能說服團隊應在推出正式版時優先執行哪項最佳化作業。
  • 也是宣傳速度的重要性。每位參與者都能瞭解程式碼方式與效能表現的相關性。

另一個優點是,Recruit 內的許多其他工程團隊也對這種實作方法感興趣,而 AirSHIFT 團隊現在正協助公司內部進行多種快速黑客松。

摘要

執行這些最佳化作業絕對不是最簡單的歷程,但 AirSHIFT 才顯得成效良好。現在,AirSHIFT 在中位數的 1.5 秒內載入 Shift 資料表,距離專案前的提升 6 倍。

執行效能最佳化作業後,有一位使用者表示:

非常感謝您加快移轉資料表的載入速度。 如今,安排輪班工作的效率大幅提升。