在開發過程中,元件的渲染效能是一個前端開發者必須面對的挑戰。或許開始接觸程式開發時,我們不太重視這一點,認為只要功能能正常運作就足夠。然而,隨著我們的工作時間增加、經驗累積,專案變得愈加複雜,頁面的載入速度和渲染效率便變得尤為重要。
因此,了解並優化 React 的渲染行為,並提升整體應用程式的效能,是一個需要學習的課題,今天也想以 React 元件渲染為例,與大家分享幾個我踩過的坑。
React 元件重新渲染的情境
開發 React 專案時,是否發生過「元件明明沒必要更新,卻還是重新渲染了一遍」這個情況?
▼ 讓我們用以下程式碼為例,這邊有一個父組件 Parent 和子組件 Child
import { useState } from "react";
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Button clicked!");
};
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child onClick={handleClick} />
<div>count: {count}</div>
</div>
);
};
const Child = ({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
};
export default Parent;
▼ 可以看到每次點擊 Increment,Child 也會跟著重新渲染

不知道大家有沒有注意到這個狀況, onClick 明明沒變,為什麼跟著渲染了呢?
先講結論:因為 handleClick 在 Parent 每次渲染時都是一個新函數,React 認為 props 變了,就觸發了子元件 (Child) 的更新。
React 渲染機制的核心概念
前面提到的「React 認為 props 變了,就觸發了子元件 (Child) 的更新」是什麼意思?
在 React 中,元件的渲染是由「狀態」或「屬性」(props) 的變化驅動的。因此 React 會在父元件渲染時,檢查傳給子元件的 props 是否跟上次不同,如果 props 的值或參考改變了,就假設這個改變可能會影響子元件的顯示,於是觸發子元件的重新渲染。
回到上述的程式碼範例,我們點擊了「Increment」按鈕,會更新 count,也意味者 Parent 父元件會重新渲染,而 Parent 每次渲染時,裡面的 handleClick 都是一個全新的函式,即使內容沒變,但他會有新的記憶體位置(也就是參考改變了)。
因為 handleClick 是 Child 子元件的 onClick prop,當 React 發現這個 props 從舊的 handleClick 變成新的 handleClick,就認為 props 變了,於是讓 Child 子元件也重新渲染。所以即使 Child 的按鈕功能沒變,也會一直執行並印出 console: Child rendered.
▼ 文字敘述可能不是很好懂,我做了一個圖表版的給大家參考這其中的影響與步驟 (會不會圖表版的更難懂 😂 )

重複渲染造成的影響
重複渲染這個問題重要嗎?試想如果今天有一個表單頁面,裡面有非常多的文字方塊,每一個文字方塊都是一個子組件,每個子元件都有一個 callback。如果不處理這個狀況,表單輸入更新後,父元件一渲染,所有子元件都會跟著重新跑一遍,很容易造成頁面卡頓的現象。
如何優化 React 的渲染問題
首先,再簡單複習一次造成這問題的根本原因,因為父元件每次渲染都會產生新的函式參考,導致子元件無端重新渲染。
這個問題的解法,有的人可能聽過,一個是 useCallback,一個是 React.memo,接下來會跟大家介紹這兩個功能以及該怎麼使用。
什麼是 useCallback
useCallback 是 React 提供的一個 Hook,用來記住一個函式,並確保該函式在多次渲染後,還能保持相同的參考(記憶體位置),除非指定的依賴項改變了。
▼ 這是 React 對 useCallback 的定義
const anyFunction = useCallback(() => {
// ...
}, [dependencies])那麼回到我們前面的例子,剛一直有重複強調,因為父元件的渲染造成 handleClick() 的參考改變,所以 React 以為他是新的 handleClick() 故重新渲染。而 useCallback 就是用來解決這個情況,幫助我們穩定函式,不要讓他的參考改變。
▼ 我們可以將 handleClick()改寫如下
const handleClick = useCallback(() => {
console.log("Button clicked!");
}, []); // 空陣列,因為不依賴任何變數什麼是 React.memo
React.memo 用來記憶一個函數元件,防止它在 props 沒有改變的情況下重新渲染
▼ 這是 React 對 React.memo 的定義
const component = React.memo((props) => {
// ...
})在 React 中,預設行為是「只要父元件重新渲染,所有的子元件都會跟著重新渲染,即使子元件的 props 沒變」。乍看沒什麼問題,但如果子元件很複雜 (比如有很多計算或很多 DOM 操作),這種不必要的渲染就會拖慢效能。
React.memo 的作用就是避免這種情況,讓子元件在真正需要時才更新。
同樣回到前面的例子,我們要將 React.memo 放在 Child 子元件中,避免不要的渲染
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
});最後加上 useCallback 以及 React.memo 的完整程式碼如下
import React, { useState, useCallback } from "react";
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Button clicked!");
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child onClick={handleClick} />
<div>count: {count}</div>
</div>
);
};
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
});
export default Parent;
▼ 此時再點擊 Increment 時,Child 子元件就不會重新渲染了

重構時的注意事項
既然 useCallback 和 React.memo 那麼好用,是不是可以在每個函式和子元件都加上去呢?
根據大家求學那麼多年的經驗來看,當有人這樣問時,表示答案一定是錯的 XXD。
useCallback 依賴項的重要性
首先,我們要確認 useCallback 的依賴項該怎麼填寫,如果 handleClick() 需要用到 count,就必須要加入依賴項:
const handleClick = useCallback(() => {
console.log("Count is:", count);
}, [count]);當然,這也意味著 count 改變時 handleClick() 會更新,此時就會回到我們一開始的問題,點擊「Increment」按鈕時,會重新渲染 Child 子元件。
因此答案沒有絕對,大家要根據自己的專案需求與程式結構,寫出正確的依賴項,才能避免後續的各種問題
React.memo 中 props 的穩定性
React.memo 的效果取決於 props 是否穩定。如果 props 本身不穩定 (比如每次渲染都會產生新的值或參考),那 React.memo 就不會生效,React.memo 不是萬靈丹,他需要 props 的搭配才能發揮作用。
那要如何讓 props 穩定?就是跟前面提到的 useCallback 有關了,我們使用 useCallback 讓 handleClick() 的參考不變,React.memo 比較 props 時發現 onClick 沒變,就不會渲染
也就是說,如果要用 React.memo,就要先用 useCallback 穩定 props。
適度優化
老實說,不是所有場景都一定要用 React.memo 或 useCallback,過度使用反而會讓程式碼變得複雜。如果元件很單純,渲染成本也低,加了 React.memo 反而會造成記憶體負擔。
大家可以試著用 React DevTools Profiler 先確認問題,再針對性處理,可以到 Chrome Extension 下載這個 Tool。
小結
最後幫大家總結 useCallback 和 React.memo 的特性:
useCallback 是幫助我們穩定函式的 Hook,特別適合解決「props 變了導致子元件重新渲染」的問題。它的核心是透過快取讓函式的參考不變,搭配 React.memo 能大幅提升效能。
React.memo 是個幫助我們優化效能的工具,讓子元件只有在 props 改變時才渲染。它透過淺比較 props 以跳過不必要的更新,跟 useCallback 是最佳拍檔。
希望今天這篇介紹,可以讓大家更了解 React 的渲染機制,有任何問題歡迎留言討論唷。
👍👍👍