高效的 React 組件:優化 React 性能的指南
已發表: 2022-03-11自推出以來,React 改變了前端開發人員構建 Web 應用程序的方式。 借助虛擬 DOM,React 使 UI 更新盡可能高效,讓您的 Web 應用程序更快捷。 但是,為什麼中等大小的 React Web 應用程序仍然表現不佳?
好吧,線索在於你如何使用 React。
像 React 這樣的現代前端庫不會神奇地讓你的應用程序更快。 它要求開發人員了解 React 的工作原理以及組件如何在組件生命週期的各個階段中生存。
使用 React,您可以通過測量和優化組件的渲染方式和時間來獲得它必須提供的大量性能改進。 而且,React 只提供了使這一切變得容易所需的工具和功能。
在本 React 教程中,您將學習如何測量 React 組件的性能並優化它們以構建性能更高的 React Web 應用程序。 您還將了解一些 JavaScript 最佳實踐如何幫助您的 React Web 應用程序提供更流暢的用戶體驗。
React 是如何工作的?
在深入研究優化技術之前,我們需要更好地了解 React 的工作原理。
在 React 開發的核心,您擁有簡單明了的 JSX 語法,以及 React 構建和比較虛擬 DOM 的能力。 自發布以來,React 影響了許多其他前端庫。 Vue.js 等庫也依賴於虛擬 DOM 的概念。
以下是 React 的工作原理:
每個 React 應用程序都從一個根組件開始,並由許多組件組成的樹形結構。 React 中的組件是基於它接收到的數據(props 和 state)呈現 UI 的“函數”。
我們可以將其表示為F
。
UI = F(data)
用戶與 UI 交互並導致數據發生變化。 無論交互是否涉及單擊按鈕、點擊圖像、拖動列表項、調用 API 的 AJAX 請求等,所有這些交互都只會更改數據。 它們永遠不會導致 UI 直接更改。
在這裡,數據是定義 Web 應用程序狀態的所有內容,而不僅僅是您存儲在數據庫中的內容。 甚至前端狀態的位(例如,當前選擇了哪個選項卡或當前是否選中了複選框)也是該數據的一部分。
每當這些數據發生變化時,React 都會使用組件函數重新渲染 UI,但只是虛擬的:
UI1 = F(data1) UI2 = F(data2)
React 通過在其虛擬 DOM 的兩個版本上應用比較算法來計算當前 UI 和新 UI 之間的差異。
Changes = Diff(UI1, UI2)
然後 React 繼續僅將 UI 更改應用到瀏覽器上的真實 UI。
當與組件關聯的數據發生變化時,React 會確定是否需要進行實際的 DOM 更新。 這允許 React 避免在瀏覽器中進行潛在的昂貴的 DOM 操作操作,例如創建 DOM 節點和不必要地訪問現有節點。
這種重複的組件差異和渲染可能是任何 React 應用程序中 React 性能問題的主要來源之一。 構建一個 React 應用程序,其中 diffing 算法無法有效協調,導致整個應用程序重複呈現,這可能會導致令人沮喪的緩慢體驗。
從哪裡開始優化?
但我們優化的究竟是什麼?
你看,在初始渲染過程中,React 構建了一個 DOM 樹,如下所示:
給定部分數據更改,我們希望 React 只重新渲染直接受更改影響的組件(甚至可能跳過其餘組件的 diff 過程):
然而,React 最終做的是:
在上圖中,所有黃色節點都被渲染和比較,導致時間/計算資源的浪費。 這是我們將主要進行優化工作的地方。將每個組件配置為僅在必要時渲染差異將使我們能夠回收這些浪費的 CPU 週期。
React 庫的開發人員考慮到了這一點,並為我們提供了一個鉤子來做到這一點:一個讓我們告訴 React 什麼時候可以跳過渲染組件的函數。
先測量
正如 Rob Pike 將其作為他的編程規則之一相當優雅地指出的那樣:
措施。 在您測量之前不要調整速度,即使這樣也不要調整,除非代碼的一部分壓倒了其餘部分。
不要開始優化您認為可能會降低應用程序速度的代碼。 相反,讓 React 性能測量工具引導您完成整個過程。
React 有一個強大的工具來解決這個問題。 使用react-addons-perf
庫,您可以了解應用程序的整體性能。
用法很簡單:
Import Perf from 'react-addons-perf' Perf.start(); // use the app Perf.stop(); Perf.printWasted();
這將打印一個表格,其中包含在渲染中浪費的組件時間量。
該庫提供了其他函數,可讓您分別打印浪費時間的不同方面(例如,使用printInclusive()
或printExclusive()
函數),甚至打印 DOM 操作操作(使用printOperations()
函數)。
讓基準測試更進一步
如果你是一個視覺型的人,那麼react-perf-tool
正是你所需要的。
react-perf-tool
基於react-addons-perf
庫。 它為您提供了一種更直觀的方式來調試 React 應用程序的性能。 它使用底層庫來獲取測量結果,然後將它們可視化為圖形。
通常,這是一種更方便的發現瓶頸的方法。 您可以通過將其作為組件添加到您的應用程序來輕鬆使用它。
React 應該更新組件嗎?
默認情況下,React 將運行,渲染虛擬 DOM,並比較樹中每個組件的差異,以了解其 props 或 state 的任何變化。 但這顯然是不合理的。
隨著您的應用程序的增長,嘗試在每個操作處重新渲染和比較整個虛擬 DOM 最終會減慢速度。
React 為開發人員提供了一種簡單的方法來指示組件是否需要重新渲染。 這就是shouldComponentUpdate
方法發揮作用的地方。
function shouldComponentUpdate(nextProps, nextState) { return true; }
當此函數為任何組件返回 true 時,它允許觸發 render-diff 過程。
這為您提供了一種控制渲染差異過程的簡單方法。 每當您需要防止組件被重新渲染時,只需從函數中返回false
即可。 在函數內部,您可以比較當前和下一組 props 和 state 以確定是否需要重新渲染:
function shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id; }
使用 React.PureComponent
為了簡化和自動化這種優化技術,React 提供了所謂的“純”組件。 React.PureComponent
完全一樣,它實現了一個React.Component
shouldComponentUpdate()
函數,具有淺的 prop 和狀態比較。

React.PureComponent
或多或少等同於這個:
class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState); } … }
由於它只執行淺比較,您可能會發現它僅在以下情況下才有用:
- 您的道具或狀態包含原始數據。
- 你的 props 和 states 有復雜的數據,但是你知道什麼時候調用
forceUpdate()
來更新你的組件。
使數據不可變
如果您可以使用React.PureComponent
但仍然有一種有效的方式來判斷任何復雜的道具或狀態何時自動更改,該怎麼辦? 這就是不可變數據結構讓生活更輕鬆的地方。
使用不可變數據結構背後的想法很簡單。 每當包含複雜數據的對象發生更改時,而不是在該對像中進行更改,而是創建具有更改的該對象的副本。 這使得檢測數據變化就像比較兩個對象的引用一樣簡單。
您可以使用Object.assign
或_.extend
(來自 Underscore.js 或 Lodash):
const newValue2 = Object.assign({}, oldValue); const newValue2 = _.extend({}, oldValue);
更好的是,您可以使用提供不可變數據結構的庫:
var map1 = Immutable.Map({a:1, b:2, c:3}); var map2 = map1.set('b', 2); assert(map1.equals(map2) === true); var map3 = map1.set('b', 50); assert(map1.equals(map3) === false);
這裡, Immutable.Map
由庫 Immutable.js 提供。
每次使用其方法set
更新映射時,僅當 set 操作更改基礎值時才會返回新映射。 否則,返回相同的地圖。
您可以在此處了解有關使用不可變數據結構的更多信息。
更多 React 應用優化技術
使用生產版本
在開發 React 應用程序時,您會看到非常有用的警告和錯誤消息。 這些使得在開發過程中識別錯誤和問題成為一種幸福。 但它們是以性能為代價的。
如果你查看 React 的源代碼,你會看到很多if (process.env.NODE_ENV != 'production')
檢查。 React 在您的開發環境中運行的這些代碼塊並不是最終用戶需要的。 對於生產環境,所有這些不必要的代碼都可以丟棄。
如果您使用create-react-app
引導您的項目,那麼您可以簡單地運行npm run build
來生成生產版本,而無需這些額外代碼。 如果你直接使用 Webpack,你可以運行webpack -p
(相當於webpack --optimize-minimize --define process.env.NODE_ENV="'production'"
。
早期綁定函數
在渲染函數中看到綁定到組件上下文的函數是很常見的。 當我們使用這些函數來處理子組件的事件時,經常會出現這種情況。
// Creates a new `handleUpload` function during each render() <TopBar onUpload={this.handleUpload.bind(this)} /> // ...as do inlined arrow functions <TopBar onUpload={files => this.handleUpload(files)} />
這將導致render()
函數在每個渲染上創建一個新函數。 一個更好的方法是:
class App extends React.Component { constructor(props) { super(props); this.handleUpload = this.handleUpload.bind(this); } render() { … <TopBar onUpload={this.handleUpload} /> … } }
使用多個塊文件
對於單頁 React Web 應用程序,我們通常最終將所有前端 JavaScript 代碼捆綁在一個壓縮文件中。 這適用於中小型 Web 應用程序。 但隨著應用程序開始增長,將這個捆綁的 JavaScript 文件傳送到瀏覽器本身可能會成為一個耗時的過程。
如果您使用 Webpack 構建您的 React 應用程序,您可以利用它的代碼拆分功能將您構建的應用程序代碼分成多個“塊”,並根據需要將它們交付給瀏覽器。
拆分有兩種類型:資源拆分和按需代碼拆分。
通過資源拆分,您可以將資源內容拆分為多個文件。 例如,使用 CommonsChunkPlugin,您可以將公共代碼(例如所有外部庫)提取到自己的“塊”文件中。 使用 ExtractTextWebpackPlugin,您可以將所有 CSS 代碼提取到單獨的 CSS 文件中。
這種分裂將在兩個方面有所幫助。 它可以幫助瀏覽器緩存那些不經常更改的資源。 它還將幫助瀏覽器利用並行下載來潛在地減少加載時間。
Webpack 一個更顯著的特性是按需代碼拆分。 您可以使用它將代碼拆分為可以按需加載的塊。 這可以使初始下載保持較小,從而減少加載應用程序所需的時間。 然後,瀏覽器可以在應用程序需要時按需下載其他代碼塊。
您可以在此處了解有關 Webpack 代碼拆分的更多信息。
在您的 Web 服務器上啟用 Gzip
React 應用的 bundle JS 文件通常很大,所以為了讓網頁加載更快,我們可以在 Web 服務器(Apache、Nginx 等)上啟用 Gzip
現代瀏覽器都支持並自動協商 HTTP 請求的 Gzip 壓縮。 啟用 Gzip 壓縮可以將傳輸響應的大小減少多達 90%,這可以顯著減少下載資源的時間,減少客戶端的數據使用量,並縮短首次呈現頁面的時間。
查看有關如何啟用壓縮的 Web 服務器文檔:
- 阿帕奇:使用 mod_deflate
- Nginx:使用 ngx_http_gzip_module
使用 Eslint-plugin-react
您應該將 ESLint 用於幾乎所有 JavaScript 項目。 React 也不例外。
使用eslint-plugin-react
,您將強迫自己適應 React 編程中的許多規則,從長遠來看,這些規則可以使您的代碼受益,並避免由於編寫不佳的代碼而發生的許多常見問題和問題。
讓您的 React Web 應用程序再次快速運行
要充分利用 React,您需要利用它的工具和技術。 React Web 應用程序的性能在於其組件的簡單性。 壓倒性的渲染差異算法會使您的應用程序以令人沮喪的方式表現不佳。
在優化應用之前,您需要了解 React 組件的工作原理以及它們在瀏覽器中的呈現方式。 React 生命週期方法為您提供了防止組件不必要地重新渲染的方法。 消除這些瓶頸,您將獲得用戶應得的應用程序性能。
儘管有更多優化 React Web 應用程序的方法,但微調組件以僅在需要時更新會產生最佳的性能改進。
你如何衡量和優化你的 React Web 應用程序性能? 在下面的評論中分享它。