- 發布正確的 React 建置版本並優化打包器(生產版本和效能分析版本)是任何嚴肅的效能優化工作的基礎。
- 使用 React DevTools 和瀏覽器效能追蹤進行效能分析,可以發現不必要的渲染、緩慢的效果和伺服器瓶頸,然後您可以針對這些瓶頸進行最佳化。
- 記憶化、不可變性和虛擬化共同作用,降低渲染頻率,減少每次渲染的工作量,並保持大型使用者介面流暢運作。
- 程式碼分割、SSR、Web Workers 和持續監控確保快速的初始載入、響應迅速的互動以及大規模下的可持續性能。
React 開箱即用時速度飛快,但隨著應用程式的成長,很容易出現一些不易察覺的效能下降問題。 這些都會將流暢的介面變成運作緩慢、耗電的怪物。冗長的清單、笨重的元件、笨拙的狀態結構以及生產環境中的偵錯版本,所有這些加起來最終會導致使用者放棄你的頁面。
好消息是,React 自備一套豐富的工具箱,用於衡量、理解和改進渲染效能。以及相關的生態系統(打包工具、效能分析器、視窗庫、Web Workers、SSR 框架),為您提供了即使在大規模應用下也能保持 UI 流暢響應所需的一切。在本指南中,我們將深入探討這些工具,展示它們如何協同工作,並重點介紹一些團隊經常忽略但卻非常值得掌握的技巧。
使用正確的 React 建置:開發、生產和效能分析。
任何 React 應用的首要效能檢查都是驗證你發布的是生產版本,而不是開發版本。開發版本包含大量友善的警告、額外的檢查和調試輔助功能,這些在編碼時非常有用,但在生產環境中速度明顯較慢且體積較大。
您可以使用 React Developer Tools 瀏覽器擴充功能來確認您正在使用的版本。當您開啟使用 React 建立的網站時,擴充功能圖示在生產環境中背景為深色,在開發環境中背景為紅色。如果您在正式網站上看到紅色圖標,則表示您的打包工具配置洩露了錯誤的建置版本。
對於使用 Create React App 建立的項目,產生最佳化的生產包就像執行建置腳本一樣簡單。它將壓縮後的軟體包輸出到 build/ 目錄。在本地開發期間,您應該堅持使用目錄。 npm start (或等效版本),並且僅執行生產版本進行部署或進行實際效能基準測試。
如果您依賴 React 和 React DOM 的 UMD 單檔案建置版本(例如在非打包環境中)請確保您包含了以.txt結尾的檔案。 .production.min.js任何未經壓縮或非生產環境的文件僅供開發使用,會為使用者帶來不必要的偵錯開銷。
打包工具:Browserify、Rollup、Brunch 和 webpack
不同的打包工具需要不同的調整才能完全啟用 React 的生產環境最佳化。但它們都遵循相同的基本想法:將環境設定為生產環境,刪除僅用於開發的分支,並壓縮產生的 JavaScript。
對於 Brunch,建議的方法是安裝一個壓縮插件,例如: terser-brunch然後使用生產環境標誌運行建置(例如,使用 -p)此配置可確保消除開發時警告,並對最終軟體包進行嚴格壓縮。
對於 Browserify,通常需要以特定順序串聯幾個轉換。首先申請 envify 全球注入 NODE_ENV="production",然後應用 uglifyify 全域清除開發匯入和程式碼路徑,最後將捆綁包透過管道傳輸。 terser 用於資料混淆和壓縮。這裡的順序很重要,因為每一步都為下一步轉換做好準備。
使用 Rollup 時,你需要將三個外掛連接起來,以實現精簡的生產建置。: replace 將環境設定為生產環境。 commonjs 允許打包 CommonJS 模組,且 terser 執行最終的壓縮和資料混淆。此組合產生一個體積小、可用於生產環境的軟體包,不包含僅供開發使用的輔助函數。
使用 webpack 4 及更高版本,啟用生產模式會自動啟動許多最佳化,包括程式碼壓縮。。 環境 mode: 'production' Terser 在底層進行了連接,只要滿足以下條件,就能實現 React 的生產行為: NODE_ENV 匹配成功。通常情況下,除非有非常特殊的需求,否則不需要添加單獨的壓縮器。
對 React 建置進行效能分析
除了常規的開發和生產版本之外,React 還提供了一個專門用於效能分析的分析版本。此變體在內部對 React 進行檢測,以便 DevTools Profiler 等工具可以收集非常詳細的計時資訊。
要在瀏覽器環境中使用效能分析版本,您需要匯入 react-dom/profiling 而不是 react-dom/client 通常情況下,你會設定一個打包別名,這樣就不需要手動修改每個導入語句。有些框架已經提供了標誌或模式來自動切換此行為。
早期版本的 React(17 之前)依賴標準的 User Timing API。 產生可在瀏覽器效能面板中顯示的標記和指標。現代 React 將這些功能與 React DevTools 中的專用分析器標籤結合,以便您可以直接深入分析元件。
理解並衡量 React 效能
你無法修復你沒有衡量過的問題,因此 React 的效能優化工作應該始終從效能分析開始。這意味著要使用瀏覽器工具和 React 專用的效能分析器來查看實際耗時在哪裡,以及哪些元件的重新渲染次數過多。
Chrome 開發者工具的效能面板是您了解瀏覽器運作狀況的基本工具。JavaScript 執行、網路請求、佈局、渲染、事件循環延遲和自訂追蹤等所有資訊都顯示在統一的時間軸上。 React 透過專門的追蹤功能整合到此視圖中,以顯示框架特定的活動。
現代 React 會公開與常規瀏覽器追蹤一致的調度器、元件和伺服器追蹤資訊。這樣可以讓你同步查看網路、JavaScript 和 React 的更新,這在你追蹤僅在負載下出現的卡頓或奇怪的停滯現象時非常有用。
調度器追蹤和渲染階段
Scheduler 是 React 內部的抽象層,用於協調不同優先順序的任務。在效能追蹤中,您會看到單獨的子軌道,分別用於阻塞性工作(通常是同步的使用者驅動更新)和過渡性工作(由…觸發的後台 UI 更新)。 startTransition)與懸念相關的任務和在沒有更緊急的事情發生時運行的空閒工作。
每個渲染過程都會經歷幾個不同的階段,您可以在時間軸上查看這些階段。:更新階段(觸發渲染的原因)、渲染階段(React 呼叫元件並建立下一樹)、提交階段(DOM 改變並套用佈局效果,例如: useLayoutEffect 運行)以及剩餘效果階段(其中被動效果如 useEffect 通常跟油漆一起跑)。
級聯更新(即在渲染過程中安排的狀態變更)是造成效能問題的典型隱患。在開發過程中,React 可以在時間軸中標記這些,甚至可以顯示哪個元件和方法安排了額外的更新,從而幫助您避免意外的渲染循環或重複工作。
組件追蹤:用於渲染和效果的火焰圖
元件追蹤圖以視覺化的方式呈現每個元件(及其子元件)的渲染時間。 使用火焰圖。圖中的區塊越寬,表示該元件子樹在該渲染過程中消耗的時間越多。
React 也將效果持續時間作為單獨的火焰圖公開。 採用與調度程式軌道中對應階段相對應的配色方案,以便您可以一眼區分渲染時間和效果時間。
諸如掛載、卸載、重新連接和斷開連接等其他事件會以註釋的形式顯示在這些火焰圖上。例如,安裝新的樹幹部分或移除樹幹部分都會被標記出來,一些特徵,例如… <Activity> 各個組件都有自己的重新連接/斷開連接標記。
在開發環境中,點擊元件軌道中的渲染條目即可查看哪些屬性發生了變化。當你試圖追蹤不必要的渲染或屬性(這些屬性不斷改變引用但實際值並未改變)時,這將非常有用。
伺服器追蹤:請求和伺服器組件
如果您正在使用 React 伺服器元件,效能工具還可以揭示伺服器端的行為。「伺服器請求」追蹤匯總了最終將資料提供給伺服器元件的 Promise,包括對以下情況的呼叫: fetch 或非同步檔案系統操作。
React 嘗試將第三方輔助函數中建立的 Promise 分組到一個 span 元素中。 所以你會看到一個邏輯運算,例如: getUser 而不是十幾個低階人員 fetch 調用。點擊 span 元素會顯示其建立位置,以及(如果可用)已解析的值或拒絕原因。
單獨的「伺服器元件」追蹤顯示伺服器元件樹及其等待的 Promise 需要多長時間也以火焰圖的形式呈現。當 React 可以並發渲染伺服器元件時,它會建立一個主軌道和額外的平行軌道;如果並發數量超過一定閾值,則會將額外的工作分組,以保持視圖的可讀性。
減少不必要的渲染:React.memo、useMemo、useCallback 和 PureComponent
React 應用中最大的、最常見的效能損耗之一就是不必要的重新渲染。每當父元件更新時,其子元件預設都會重新渲染,即使它們的輸入(屬性)相同,輸出 DOM 實際上也不會改變。
React 提供了多種工具來減少這種浪費的工作: React.memo 對於功能組件, React.PureComponent 對於類別組件,以及 useMemo/useCallback 用於穩定作為 props 傳遞的值的鉤子這些方法並不能神奇地解決所有效能問題,但如果使用得當,它們可以帶來巨大的改變。
React.memo 包裝一個函數式元件,當其 props 與前一個元件的 props 淺相等時,跳過重新渲染。當元件經常使用相同的 props 進行渲染、具有繁重的渲染邏輯,或者效能分析器顯示它是瓶頸時,這種方法最有價值。
當你將組件記憶化時,也需要確保它的 props 不會不必要地改變其身分。在每次渲染時,在父 JSX 中建立新物件或內聯函數都會使淺比較失效,並強制子元素重新渲染,即使邏輯資料相同。
這是哪裡 useMemo 以及 useCallback 進來吧: useMemo 穩定從其他狀態派生的物件或陣列值,使其僅在依賴項發生變化時才發生變化,並且 useCallback 為傳遞給記憶化子函數的回呼函數提供穩定的函數參考。
類別元件:shouldComponentUpdate 和 React.PureComponent
從底層來看,大多數 React 渲染最佳化都歸結為控制是否 shouldComponentUpdate 返回真或假預設實作總是會傳回 true,這表示任何 prop 或 state 的變更都會觸發該元件及其子樹的渲染和協調。
透過覆蓋 shouldComponentUpdate對於不需要更新的子樹,您可以省略一些工作。如果傳回 false,React 將不會調用 render() 對於該元件或其任何後代,它甚至不會比較樹的該部分的新舊虛擬 DOM 節點。
考慮一個小型元件樹,其中一些節點從該元件樹傳回 false。 shouldComponentUpdateReact 可以完全跳過這些分支的遍歷,而方法傳回 true 的其他節點則會被完整處理。最終,只有渲染輸出實際改變的節點才會導致 DOM 變更。
因為編寫自訂 shouldComponentUpdate 邏輯重複,React 出貨 React.PureComponent它實現了對當前和先前 props 及 state 的淺層比較。如果淺層比較沒有變化,React 可以安全地跳過重新渲染該類別元件。
不可變性以及為什麼淺比較會失敗
淺比較假設如果一個值發生變化,它的引用也會改變-但當你直接修改現有陣列或物件時,這個假設就不成立了。當把基於不可變性的最佳化與可變資料結構結合時,這是一個典型的錯誤來源。
試想一個 ListOfWords 接收組件 words 數組並以逗號分隔的方式渲染它們與父母配對 WordAdder 組件會將一個新單字加到同一個陣列中。如果 ListOfWords 擴展 PureComponent淺比較會看到相同的陣列引用,並認為沒有任何變化,因此 UI 不會更新。
解決方法是避免直接修改 props 或 state,而是在資料變更時建立新的陣列或物件。。 代替 words.push(newWord)你會使用 words.concat(newWord) 或者擴充語法 [...words, newWord]這會為數組建立一個新的引用,並觸發正確的更新。
同樣的原理也適用於物體。而不是重新分配 colormap.right = 'blue' 對於一個已存在的對象,您可以使用以下方式傳回一個新對象: Object.assign({}, colormap, { right: 'blue' }) 或者物件展開語法 { ...colormap, right: 'blue' }這樣就能確保淺層比較能夠辨識出新的參照物並辨識出這種變化。
當資料嵌套很深時,手動維護資料的不可變性會變得非常繁瑣。像 Immer 或 immutability-helper 這樣的函式庫允許你編寫看起來像命令式和可變的程式碼,同時在內部產生新的不可變結構,這與…配合得很好 PureComponent 以及 React.memo.
虛擬化長列表和複雜的使用者介面
一次渲染數百個 DOM 節點是導致 React 效能急劇下降的最快方法之一。尤其是在低階設備上,或與複雜的佈局和影像結合使用時,效能損耗會更大。即使採用高效的協調機制,僅是記憶體和螢幕上顯示如此多的節點也會消耗大量資源。
視窗化(或清單虛擬化)透過僅渲染視窗中目前可見的清單部分來解決這個問題。當使用者捲動頁面時,React 會掛載進入視圖的新元素,並卸載滾動出視圖的元素,從而保持渲染行數大致恆定。
熱門圖書館 react-window 以及 react-virtualized 為清單、網格和表格提供可重複使用的元件 它們實現了高效的虛擬化策略。它們負責處理渲染哪些項目、尺寸調整、滾動容器,甚至無限載入行為等數學計算。
虛擬化的設定通常涉及三個部分。選擇合適的組件(例如, FixedSizeList 對於均勻的行或 VariableSizeList 對於動態高度),賦予容器一個固定高度。 overflow: scroll並且只渲染庫請求的 item 元件,通常使用 memosive 進行快取。 React.memo 避免不必要的重新渲染。
如果運用得當,即使處理大量資料集,虛擬化也能保持流暢的滾動效能和較低的記憶體佔用。現實世界中的應用程式已經利用這種技術有效地瀏覽大量內容(音樂評論、電子商務目錄、收件匣),而不會出現使用者介面卡頓的情況。
虛擬清單的可訪問性確實需要額外注意。您需要確保鍵盤導航功能正常,在項目掛載和卸載時焦點管理正確,並且螢幕閱讀器可以透過 ARIA 屬性獲得足夠的上下文資訊來理解清單中目前可見的部分。
狀態管理、虛擬 DOM 和元件結構
虛擬 DOM 常被誤解為萬靈藥,但它實際上只是一個智慧的差異比較層。React 會維護 UI 的記憶體表示,並將新樹與舊樹進行比較,以確定哪些 DOM 操作是絕對必要的。
即使效率如此之高,每次渲染和差異比較仍然會耗費時間,因此你的目標是盡量減少大型子樹需要重新渲染的次數。這就是狀態管理、元件邊界和記憶化策略的交會點。
首先,根據應用程式的複雜程度選擇合適的狀態管理策略。.本地 React 狀態(useState, useReducer對於小型元件來說,它體積小巧且簡單;而像 Redux 這樣的函式庫或像 Zustand 這樣的輕量級存儲,則可以透過最佳化的訂閱模式集中管理更複雜的全域狀態。
其次,合理組織你的狀態,使相關數據分組得當。有時這意味著合併多個 useState 呼叫單一物件是為了使更新保持一致;在其他情況下,拆分狀態,使獨立的關注點不會互相強制重新渲染,會更加有效。
更新基於先前值的狀態時,請務必使用函數式更新。 如 setCount(prev => prev + 1)並透過克隆數組和物件而不是直接修改它們來保持不可變性。這帶來了可預測的行為,並且與記憶化和 PureComponents 配合良好。
一條實用的經驗法則是盡可能保持狀態的局部性。狀態值儲存在元件樹的層級越高,每次狀態值改變時需要重新渲染的元件就越多。將狀態下推到實際使用它的元件,可以限制每次更新的影響範圍。
最後,將大型組件拆分成更小、更專注的組件,這些組件的屬性很少會改變。具有穩定屬性的記憶化葉子組件減少了 React 需要差異比較的虛擬 DOM 數量,並縮短了 DOM 更新的路徑,從而減少了 DOM 更新的次數。
程式碼拆分、延遲加載和更有效率的資源加載
JavaScript 套件的大小是導致效能下降的主要原因之一,尤其是在行動網路上。如果你的 React 套件需要幾秒鐘才能下載和解析,用戶在看到你漂亮的 UI 之前就會離開。
使用程式碼分割 React.lazy 以及 Suspense 透過按需加載組件而不是預先運送所有組件,可以實現這一點。與其將所有功能都打包到初始有效負載中,不如動態導入僅特定路由或互動所需的部分。
常見的策略是路由級拆分其中每個頁面都是獨立的程式碼區塊,僅在使用者導航到該頁面時才會載入。你還可以更進一步,拆分大型功能組件或不常用的面板,只要將它們包裹在 `<div>` 標籤內即可。 Suspense 並配備合適的備用使用者介面。
延遲載入也適用於圖片。。 新增中 loading="lazy" 至 <img> 標籤會延遲載入首屏下方的圖片,直到它們滾動到螢幕上方,從而節省頻寬並加快初始渲染速度。對於更高級的效果,可以使用諸如以下的庫: react-lazy-load-image-component 支援模糊佔位符和漸進式載入。
在實現程式碼分割時,平衡程式碼區塊大小和使用者體驗至關重要。過度拆分會導致請求數量過多且過於細小,而拆分不足則會導致初始打包體積過大。因此,為惰性組件設定良好的回退機制和錯誤邊界至關重要,這樣才能避免網路請求失敗導致整個應用程式崩潰。
伺服器端渲染、React 伺服器元件和伺服器操作
伺服器端渲染 (SSR) 在伺服器端渲染你的 React 應用,並將 HTML 傳送給客戶端,這可以顯著提升使用者體驗和 SEO。使用者能更快看到有用的內容,搜尋引擎也能更可靠地索引您的網頁。
Next.js 等框架使得 SSR 和串流 HTML 在日常應用中變得實用。. 你從伺服器取得數據,將元件渲染成 HTML(有時甚至是串流渲染),然後在客戶端載入該標記,使其具有互動性。
除了傳統的伺服器端渲染 (SSR) 之外,React 伺服器元件還將更多 UI 邏輯推到伺服器端。這樣一來,你就可以渲染那些根本不會傳送給客戶端的元件。這可以顯著減小客戶端套件的大小,並簡化資料獲取,因為伺服器元件可以直接呼叫資料庫或 API。
伺服器操作擴展了這個概念,讓您定義在伺服器端運行但由客戶端元件觸發的函數。這樣可以省去很多樣化板 REST 端點或自訂 API 處理程序,並簡化您處理變更、表單提交和其他有狀態操作的方式。
SSR、伺服器元件和伺服器操作結合使用,可提供一系列渲染策略。關鍵內容可以從伺服器快速串流傳輸,繁重的邏輯不會在客戶端運行,React 運行時會將所有內容整合到一個連貫的使用者體驗中。
利用 Web Workers 卸載繁重工作
即使是優化最好的 React 樹,如果在主執行緒上執行 CPU 密集型任務,也會出現卡頓。昂貴的計算會阻塞渲染,延遲事件處理,並使您的應用程式感覺無響應。
Web Workers 提供了一種將這些繁重任務移至後台執行緒的方法。你將資料傳送給工作線程,讓它處理資料或大型資料集,然後透過訊息傳遞接收結果,讓主線程可以自由地處理 UI 更新。
Web Workers 的典型工作負載包括資料處理、影像處理、即時分析或複雜模擬。例如,使用 Web 技術堆疊建立的遊戲通常會將核心遊戲邏輯委託給一個工作線程,而主線程則專門用於渲染和輸入處理。
將 worker 與 React 整合需要建立一個單獨的腳本文件,並監聽該腳本文件的內容。 onmessage 在工作線程內部以及從元件發布訊息在元件中,您實例化工作進程,並向其發送輸入。 postMessage 當元件回應時更新狀態,理想情況下,在元件卸載時清理 worker。
像 Comlink、workerize 或 bundler 外掛這樣的函式庫可以簡化這種模式 透過抽象化底層訊息傳遞,為您提供一個感覺像是呼叫非同步函數的 API,這在 React 程式碼庫中更容易理解。
需要關注的關鍵瀏覽器和以用戶為中心的指標
從更高層面來看,整體網站效能通常使用以使用者為中心的指標進行追蹤。 例如首次內容繪製 (FCP)、最大內容繪製 (LCP) 和可互動時間 (TTI)。這些指標可以幫助您了解用戶看到內容的速度以及他們實際能夠與之互動的時間。
健康的 React 應用在典型裝置上的目標是:首次反應時間 (FCP) 低於 1.8 秒,最後一次反應時間 (LCP) 低於 2.5 秒,反應時間 (TTI) 遠低於 4 秒。不過,具體閾值可能因項目而異。如果持續超過這些數值,則表示您的資源包、渲染策略或伺服器回應時間需要改進。
Lighthouse、WebPageTest 和 Chrome 的效能面板等工具可以幫助您在合成測試環境中測量這些指標。為了獲得真實世界的洞察,SpeedCurve、Datadog、LogRocket 或 Sentry 等真實用戶監控 (RUM) 工具可以追蹤實際用戶會話,並將緩慢的體驗與程式碼變更聯繫起來。
React 隨附的 Profiler API 與這張圖片完美整合。你可以用布料包裹樹的一部分 <Profiler>記錄緩慢的渲染過程,並將其與特定的使用者流程關聯起來。結合後端和網路監控,這可以提供完整的端到端效能視圖。
效能調優的實用團隊工作流程
在實際專案中,效能調優最好被視為一個可重複的工作流程,而不是一次性的清理工作。一個簡單的四階段循環——識別、調查、實施、確認——有助於防止隨機的微優化,並將精力集中在重要的事情上。
識別是指利用分析器、指標和使用者報告來發現特定症狀。 例如頁面載入緩慢、幀率低或某些流程中使用者放棄率高。你需要的是可量化的問題,而不是憑感覺。
調查深入探究根本原因可能是頁面中包含數十個隱藏的 iframe,某個元件渲染過於頻繁,或者每個路由都載入了一個龐大的第三方函式庫。這時就需要大量使用 React DevTools Profiler 和 Chrome 的時間軸功能了。
實施階段是指應用針對性修復措施的階段。——例如,快取熱門元件、虛擬化長清單、分割資源包、將工作卸載到 Web Worker,或為特定頁面啟用 SSR。每項更改都應該足夠小,以便於理解。
確認是最後一步,也是最容易被忽略的一步。. 您重新執行分析場景並檢查指標儀表板,以確保變更確實改善了數據,並且沒有在系統的其他方面引入退化。
當你將正確的 React 建置、周全的記憶化、不可變狀態實踐、清單虛擬化、策略性的程式碼分割、服務端渲染 (SSR)、Web Workers 和持續的效能監控結合起來時,最終你會得到即使變得越來越複雜的 React 應用,它們也能保持快速回應。上述技術並非旨在過早進行微調,而是為了建立一種架構,使效能成為自然而然的副產品,而不是不斷地疲於應對。


