改進和優化 React 應用程序性能的方法
React 使 Web 應用程序能夠快速更新其用戶界面 (UI),但這並不意味著您的中型或大型 React 應用程序將有效地執行。它的性能將取決於您在構建它時如何使用 React,以及您對 React 的運行方式以及組件在其生命週期的各個階段所經歷的過程的理解。 React 為 Web 應用程序提供了許多性能改進,您可以通過各種技術、功能和工具來實現這些改進。
在本教程中,我們將討論在 React 應用程序中優化性能的各種方法,以及我們可以用來提高性能的 React 特性。
從哪裡開始優化 React 應用程序的性能?
如果不確切知道何時何地進行優化,我們就無法開始優化應用程序。你可能會問,“我們從哪裡開始?”
在初始渲染過程中,React 構建組件的 DOM 樹。因此,當 DOM 樹中的數據發生變化時,我們希望 React 僅重新渲染受更改影響的那些組件,而跳過樹中未受影響的其他組件。
然而,React 最終可能會重新渲染 DOM 樹中的所有組件,即使並非所有組件都受到影響。這會導致加載時間變長,浪費時間,甚至浪費CPU資源。我們需要防止這種情況發生。因此,這是我們將重點優化的地方。
在這種情況下,我們可以將每個組件配置為僅在必要時渲染或區分,以避免浪費資源和時間。
衡量績效
永遠不要根據你的感受開始你的 React 應用程序的優化過程。相反,請使用可用的測量工具來分析您的 React 應用程序的性能,並獲取可能導致其變慢的詳細報告。
使用 Chrome 的性能選項卡分析 React 組件
根據 React 的文檔,當您仍處於開發模式時,您可以使用 Chrome 瀏覽器中的“性能”選項卡來可視化 React 組件如何安裝、更新和卸載。例如,下圖顯示了 Chrome 的“性能”在開發模式下對我的博客進行標籤分析和分析。
為此,請按以下步驟操作:
- 暫時禁用所有擴展,尤其是 React 開發者工具,因為它們會干擾分析結果。您可以通過在隱身模式下運行瀏覽器來輕鬆禁用擴展程序。
- 確保應用在開發模式下運行。也就是說,應用程序應該在您的本地主機上運行。
- 打開 Chrome 的開發者工具,點擊“性能”標籤,然後點擊“記錄”按鈕。
- 執行您想要分析的操作。錄製時間不要超過 20 秒,否則 Chrome 可能會掛起。
- 停止錄製。
- React 事件將分組在“用戶計時”標籤下。
分析器中的數字是相對的。大多數時間和組件將在生產中更快地呈現。不過,這應該可以幫助您了解何時錯誤地更新了 UI,以及 UI 更新發生的深度和頻率。
React 開發者工具分析器
根據 React 的文檔,在 react-dom
16.5+ 和 react-native
0.57+,使用 React Developer Tools Profiler 在開發者模式下可以使用增強的分析功能。 Profiler 使用 React 的實驗性 Profiler API 來整理每個渲染組件的時間信息,以便識別 React 應用程序中的性能瓶頸。
只需為您的瀏覽器下載 React Developer Tools,然後您就可以使用它附帶的分析器工具。分析器只能在開發模式或 React v16.5+ 的生產分析構建中使用。下圖是我的博客在開發模式下使用 React Developer Tools Profiler 的 profiler 總結:
為此,請按以下步驟操作:
- 下載 React 開發者工具。
- 確保您的 React 應用程序處於開發模式或處於 React v16.5+ 的生產分析版本中。
- 打開 Chrome 的“開發者工具”標籤。 React Developer Tools 提供了一個名為“Profiler”的新選項卡。
- 單擊“記錄”按鈕,然後執行您要配置的操作。理想情況下,在您執行要分析的操作後停止錄製。
- 將顯示一個圖表(稱為火焰圖),其中包含您的 React 應用的所有事件處理程序和組件。
注意 :更多信息請參見文檔。
使用 React.memo()
進行記憶
React v16 發布了一個額外的 API,一個名為 React.memo()
的高階組件 .根據文檔,這僅作為性能優化存在 .
它的名字,“備忘錄 ”來自記憶化,它基本上是一種優化形式,主要用於通過存儲昂貴函數調用的結果並在相同的昂貴函數時返回存儲的結果來加速代碼 再次調用。
記憶是一種執行一次函數的技術,通常是一個純函數,然後將結果保存在內存中。如果我們嘗試再次執行該函數,使用 與以前相同的參數 ,它只會返回第一個函數執行時之前保存的結果,而不會再次執行該函數。
將上面的描述映射到 React 生態系統中,提到的函數是 React 組件,參數是 props。
使用 React.memo()
聲明的組件的默認行為 是它僅在組件中的道具發生更改時才呈現。它對 props 進行了淺層比較來檢查這一點,但可以使用一個選項來覆蓋它。
React.memo()
通過避免重新渲染 props 未更改或不需要重新渲染的組件來提高 React 應用程序的性能。
下面的代碼是React.memo()
的基本語法 :
const MemoizedComponent = React.memo((props) => {
// Component code goes in here
})
何時使用React.memo()
- 純功能組件
您可以使用React.memo()
如果你的組件是功能性的,被賦予相同的道具,並且總是呈現相同的輸出。您也可以使用React.memo()
在帶有 React 鉤子的非純功能組件上。 - 組件經常渲染
您可以使用React.memo()
包裝一個經常渲染的組件。 - 組件使用相同的 props 重新渲染
使用React.memo()
包裝在重新渲染期間通常提供相同道具的組件。 - 中到高元素
將它用於包含中到大量 UI 元素的組件,以檢查 props 是否相等。
注意 :在記憶使用 props 作為回調的組件時要小心。確保在渲染之間使用相同的回調函數實例。這是因為父組件可以在每次渲染時提供不同的回調函數實例,這將導致記憶過程中斷。要解決此問題,請確保記憶化組件始終接收相同的回調實例。
讓我們看看我們如何在現實世界中使用記憶。下面的功能組件,稱為“照片”,使用 React.memo()
以防止重新渲染。
export function Photo({ title, views }) {
return (
<div>
<div>Photo title: {title}</div>
<div>Location: {location}</div>
</div>
);
}
// memoize the component
export const MemoizedPhoto = React.memo(Photo);
上面的代碼由一個功能組件組成,該組件顯示一個包含照片標題和照片中主題位置的 div。我們還通過創建一個新函數並將其命名為 MemoizedPhoto
來記憶組件 .記憶照片組件將防止組件重新渲染,只要道具,title
, 和 location
後面的渲染都是一樣的。
// On first render, React calls MemoizedPhoto function.
<MemoizedPhoto
title="Effiel Tower"
location="Paris"
/>
// On next render, React does not call MemoizedPhoto function,
// preventing rendering
<MemoizedPhoto
title="Effiel Tower"
location="Paris"
/>
在這裡,React 只調用了 memoized 函數一次。只要 props 保持不變,它就不會在下一次調用中渲染組件。
捆綁和縮小
在 React 單頁應用程序中,我們可以將所有 JavaScript 代碼打包並壓縮到一個文件中。這個沒問題,只要我們的應用比較小。
隨著我們的 React 應用程序的增長,將我們所有的 JavaScript 代碼捆綁和壓縮到一個文件中變得有問題、難以理解和乏味。它還會影響我們的 React 應用程序的性能和加載時間,因為我們正在向瀏覽器發送一個大的 JavaScript 文件。因此,我們需要一些流程來幫助我們將代碼庫拆分為各種文件,並根據需要將它們按時間間隔交付給瀏覽器。
在這種情況下,我們可以使用某種形式的資產捆綁器,例如 Webpack,然後利用其代碼拆分功能將我們的應用程序拆分為多個文件。
Webpack 的文檔中建議將代碼拆分作為一種改進應用程序加載時間的方法。 React 的文檔中也建議延遲加載(只提供用戶當前需要的東西),這可以顯著提高性能。
Webpack 提出了三種通用的代碼拆分方法:
- 入口點
使用條目配置手動拆分代碼。 - 防止重複
使用SplitChunksPlugin
去重複和分割塊。 - 動態導入
通過模塊內的內聯函數調用拆分代碼。
代碼拆分的好處
- 拆分代碼有助於瀏覽器的緩存資源和不經常更改的代碼。
- 它還有助於瀏覽器並行下載資源,從而減少應用程序的整體加載時間。
- 它使我們能夠將代碼分割成塊,這些塊將根據需要或應用程序的需要加載。
- 它使首次渲染時資源的初始下載相對較小,從而減少了應用的加載時間。
不可變數據結構
React 的文檔談到了不改變數據的力量。任何無法更改的數據都是不可變的。不變性是 React 程序員應該理解的一個概念。
不可變的值或對像不能更改。因此,當有更新時,會在內存中創建一個新值,而舊值保持不變。
我們可以使用不可變數據結構和React.PureComponent
自動檢查復雜的狀態變化。例如,如果您的應用程序中的狀態是不可變的,您實際上可以使用 Redux 等狀態管理庫將所有狀態對象保存在單個存儲中,從而使您能夠輕鬆實現撤消和重做功能。
不要忘記,一旦創建不可變數據,我們就無法更改它。
不可變數據結構的好處
- 它們沒有副作用。
- 不可變數據對象易於創建、測試和使用。
- 它們幫助我們編寫可用於快速檢查狀態更新的邏輯,而無需一遍又一遍地檢查數據。
- 它們有助於防止時間耦合(一種代碼依賴於執行順序的耦合)。
以下庫有助於提供一組不可變的數據結構:
- 不變性助手
在不更改來源的情況下更改數據副本。 - 不可變的.js
用於 JavaScript 的不可變持久數據集合提高了效率和簡單性。 - 無縫-不可變
JavaScript 的不可變數據結構與普通的 JavaScript 數組和對象向後兼容。 - 反應-複製-寫入
這通過可變 API 提供了不可變狀態。
其他提高性能的方法
在部署前使用生產構建
React 的文檔建議在部署您的應用時使用最小化的生產構建。
避免使用匿名函數
因為匿名函數沒有分配標識符(通過 const/let/var
),只要組件不可避免地再次渲染,它們就不會持久化。這會導致 JavaScript 在每次重新渲染此組件時分配新內存,而不是像使用命名函數時那樣只分配一次內存。
import React from 'react';
// Don’t do this.
class Dont extends Component {
render() {
return (
<button onClick={() => console.log('Do not do this')}>
Don’t
</button>
);
}
}
// The better way
class Do extends Component {
handleClick = () => {
console.log('This is OK');
}
render() {
return (
<button onClick={this.handleClick}>
Do
</button>
);
}
}
上面的代碼顯示了兩種不同的方法來使按鈕在單擊時執行操作。第一個代碼塊使用 onClick()
中的匿名函數 道具,這會影響性能。第二個代碼塊使用 onClick()
中的命名函數 函數,這是本場景中正確的方式。
安裝和卸載組件通常很昂貴
不建議使用條件或 Tenaries 使組件消失(即卸載它),因為使組件消失會導致瀏覽器重繪和重排。這是一個代價高昂的過程,因為必須重新計算文檔中 HTML 元素的位置和幾何形狀。相反,我們可以使用 CSS 的 opacity
和 visibility
隱藏組件的屬性。這樣,組件仍然在 DOM 中但不可見,沒有任何性能成本。
虛擬化長列表
文檔建議,如果您要渲染包含大量數據的列表,則應在可見視口內一次渲染列表中的一小部分數據。然後,您可以在列表滾動時呈現更多數據;因此,數據僅在視口中時才會顯示。這個過程稱為“窗口化”。在窗口化中,在任何給定時間都會呈現一小部分行。有一些流行的庫可以做到這一點,其中兩個由 Brian Vaughn 維護:
- 反應窗口
- 反應虛擬化
結論
還有其他幾種方法可以提高 React 應用程序的性能。本文討論了最重要和最有效的性能優化方法。
我希望你喜歡閱讀本教程。您可以通過下面列出的資源了解更多信息。如果您有任何問題,請將其留在下面的評論部分。我很樂意一一解答。
參考資料和相關資源
- “優化性能”,React 文檔
- “明智地使用 React.memo”,Dmitri Pavlutin
- “React 中的性能優化技術”,Niteesh Yadav
- “React 中的不變性:變異對像沒有錯”,Esteban Herrera
- “優化 React 應用性能的 10 種方法”,Chidume Nnamdi
- “提高 React 應用性能的 5 個技巧”,William Le