在前一篇文章中,我們介紹了如何利用 ThemeProvider
和自定義 Hook useTheme
來集中管理應用的主題狀態。隨著應用規模的擴大,我們開始管理更多的狀態,例如語言切換和用戶設置等。雖然 Context API 是一個集中管理狀態的強大工具,但當不同的上下文被頻繁使用時,可能會面臨性能問題:不必要的元件重繪。
在本篇文章中,我們將探討這些重繪問題是如何發生的,並通過一些優化技巧來避免不必要的性能浪費。
React 的核心特性之一是其“按需渲染”機制,當狀態改變時,只會重新渲染那些受影響的元件。然而,當我們使用 Context API 來集中管理應用狀態時,任何依賴該 Context 的元件都會在 Context 狀態變更時重新渲染,無論它們是否需要更新。例如,假設我們的應用使用 ThemeContext
來管理 isDarkMode
和 toggleTheme
,當用戶切換語言時,即使主題狀態(isDarkMode
)沒有改變,所有依賴 ThemeContext
的組件仍可能會被觸發重繪,導致性能浪費。
為了確定哪些組件發生了不必要的重繪,我們可以在元件內部加入 console.log
語句,觀察每次渲染的行為。例如,對於 App
元件,我們可以這樣寫:
if (process.env.NODE_ENV !== 'production') {
console.log('App rendered');
}
同樣地,我們可以在 Logo
、ThemeButton
和 LangButton
等組件中加入這些日誌,當語言切換時,觀察哪些組件被重繪。如下圖,打開瀏覽器控制台顯示Logo
、 LangButton
仍然發生重繪。
React 提供了幾個實用的工具來減少不必要的重繪,以下是常用的三個:
React.memo
:高階組件,用於記住元件的輸出,僅當 props 改變時才會觸發重新渲染。useMemo
:記住某個值的計算結果,僅在依賴變更時重新計算,避免每次渲染都重新計算不必要的值。useCallback
:記住函數的引用,僅在依賴變更時才會生成新的函數,減少不必要的函數創建。React.memo
和 useCallback
優化React.memo
優化 Logo
當語言切換時,Logo
不應該重新渲染,因為它與語言狀態無關。我們可以通過 React.memo
來優化:
import React from 'react'
import * as styles from './Logo.module.scss';
import { useTheme } from '@/utils/ThemeContext';
const Logo = ({ }) => {
// 使用 require 導入圖片
const logoDark = require('@/assets/logo_dark.png');
const logoLight = require('@/assets/logo_light.png');
const { isDarkMode } = useTheme();
if (process.env.NODE_ENV !== 'production') {
console.log('Logo rendered');
}
return (
<img src={isDarkMode ? logoDark : logoLight}
alt="Logo" className={styles.img} />
)
}
export default React.memo(Logo);
透過 React.memo
,Logo
元件僅在 isDarkMode 變化時重新渲染,避免因語言切換而重繪。
React.memo
優化 Version
元件接著,我們新增一個 Version 的元件,其主要功能是顯示應用的版本號。由於這個元件不會受到主題或語言等其他應用狀態的影響,我們可以設計它成為一個完全獨立的元件,避免不必要的重渲染。為了進一步優化,我們使用 React.memo
來包裹這個元件,確保它只有在版本號發生變更時才會重新渲染,而不會因主題或語言的變更被觸發。
我們使用 Webpack 的 DefinePlugin
將版本號從 package.json
中注入到應用的環境變量中。通過這樣的設置,Version
組件可以直接從環境變量中讀取並顯示版本號,無需手動更新。
const webpack = require('webpack');
const packageJson = require('./package.json');
module.exports = {
//... 其他配置
plugins: [
new webpack.DefinePlugin({
'process.env.REACT_APP_VERSION': JSON.stringify(packageJson.version),
}),
],
};
在這個配置中,我們使用 DefinePlugin
將應用的版本號轉換成一個環境變量,這樣可以在應用中通過 process.env.REACT_APP_VERSION
訪問版本號。
接著,我們在 Version
組件中使用這個變量來顯示版本號,並用 React.memo
來優化渲染。具體代碼如下:
import React from 'react';
const Version = () => {
const appVersion = process.env.REACT_APP_VERSION;;
if (process.env.NODE_ENV !== 'production') {
console.log('Version rendered');
}
return (
<div>
<p>App Version: {appVersion}</p>
</div>
);
};
export default React.memo(Version);
這樣的設計可以確保 Version
組件只在版本號變更時重新渲染,減少不必要的性能浪費。同時,使用環境變量來顯示版本號,讓應用在每次構建時自動更新版本號,簡化了維護工作。
ThemeProvider
在 ThemeProvider
中,我們可以使用 useCallback
來優化 toggleTheme
函數,確保它的引用只在 isDarkMode
改變時才更新。
// src/utils/ThemeContext.js
import React, { createContext, useState, useContext, useCallback } from 'react';
// 創建一個主題上下文
const ThemeContext = createContext();
// ThemeProvider 組件負責提供主題狀態和切換功能
export const ThemeProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(false);
// 使用 useCallback 優化 toggleTheme,確保函數只在依賴變更時重新生成
const toggleTheme = useCallback(() => {
setIsDarkMode(prevMode => !prevMode);
}, []);
return (
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// 自定義 Hook,便於其他組件訪問主題上下文
export const useTheme = () => useContext(ThemeContext);
useCallback
確保 toggleTheme
只有在依賴(isDarkMode
)發生變化時才會重新創建,這樣可以避免由於函數重新創建導致的不必要重繪。
通過本文,我們探討了如何使用 useMemo
和 useCallback
來減少不必要的重繪,從而提升 React 應用的性能。這些 Hooks 對於應用中的性能優化非常有幫助,特別是在 ThemeProvider
這樣的場景中,能夠有效地減少由 Context 變更引發的重複渲染。
在完成這些性能優化後,你是否注意到開發過程中的另一個潛在問題:過多的日誌輸出?如何控制日誌的數量與等級,避免在生產環境中出現不必要的日誌?在應用中,是否有一個靈活的方法來根據不同的環境設定日誌輸出級別,以減少運行時的性能影響?
在下一篇文章中,我們將探討如何通過環境變數和日誌等級控制來優化日誌管理。這不僅能夠減少不必要的日誌輸出,還可以確保在開發與生產環境中有針對性的日誌輸出,進一步提升應用的性能和可維護性。
此外,完整的程式碼實作已上傳至 GitHub,歡迎大家前往查看並挑戰更多優化練習。
👉 前往 GitHub 的 v0.8.0-context-render-optimization查看完整代碼
✨ 流光館Luma<∕> ✨ 期待與你繼續探索更多技術知識!