iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
JavaScript

React 觀念架構:從js 基礎到Hook 底層邏輯 系列 第 13 篇

Day 13 Render 瀑布流與效能坑:為什麼 Parent Render 會引發 Child 渲染?

  • 分享至 

  • xImage
  •  

今天要深入探討這個機制引發的連鎖反應——Render 瀑布流(Render Waterfall),並示範如何利用「架構拆分」在不使用 React.memo 的情況下優雅化解效能坑。

一、 什麼是 Render 瀑布流?

在 React 中,預設的渲染行為是自頂向下(Top-Down)遞迴進行的。

當頁面頂層的父組件發生 State 更新時,React 會重新執行該父組件的函數,並順著 Component Tree 一路向下,無條件呼叫所有子組件、孫組件的函數,直到整棵樹被走訪完畢。

[Parent]  ◄── State 變更,觸發 Re-render
        /      \
    [ChildA]  [ChildB]  ◄── 預設全部被迫連帶 Re-render!
      /          \
  [GrandChild]  [GrandChild]

為什麼 React 要預設「連帶渲染」?

這不是 React 的 Bug,而是一種安全設計(Correctness by Default)。React 無法預先知道子組件內部是否依賴了父組件的隱性狀態(例如 Context 或傳遞的 Props)。為了確保 UI 與資料永遠保持 100% 一致,預設對整棵子樹進行 Re-render 是最安全的選擇。

二、 =被高頻 State 拖垮的重型組件

假設我們有一個頁面,頂層維護著一個滾動位置(scrollY)或文字輸入(inputValue)等高頻變動的 State,而底下掛載了一個計算量極大的卡片列表:

// 效能坑寫法:高頻 State 與重型 UI 混在一起
function App() {
  const [inputValue, setInputValue] = useState('');

  return (
  
  三、 不靠 memo 的架構優化法:State 下沉 (Moving State Down)
    <div>
      {/* 每次打字,inputValue 都會更新 */}
      <input 
        value={inputValue} 
        onChange={(e) => setInputValue(e.target.value)} 
      />
      
      {/* 雖然 HeavyList 根本沒用到 inputValue,但每次打字都會被強迫 Re-render! */}
      <HeavyList />
    </div>
  );
}

結果:使用者每打一個字,App 就 Re-render,連帶導致 重新執行,造成打字體驗嚴重 lag!

三、 不靠 memo 的架構優化法:State 下沉 (Moving State Down)

要阻止 Render 瀑布流,最直覺的做法是將 State 下沉到真正需要它的最小子組件中,阻斷對外層父組件的影響。

// 1. 將高頻 State 與 Input 獨立封裝成子組件
function SearchInput() {
  const [inputValue, setInputValue] = useState('');
  return (
    <input 
      value={inputValue} 
      onChange={(e) => setInputValue(e.target.value)} 
    />
  );
}

// 2. 父組件保持乾淨,不再維護高頻 State
function App() {
  return (
    <div>
      <SearchInput /> {/* 打字時,只有 SearchInput 自己會 Re-render */}
      <HeavyList />   {/* 完全不受影響! */}
    </div>
  );
}

四、 進階架構優化法:內容抽離 (Lift Content Up via children)

如果父組件必須維護 State(例如負責滾動監聽或手勢滑動),但又不想影響包含的子內容,可以使用 children 傳遞技巧:

// 將包含 State 的容器組件抽出來,並接受 children
function ScrollContainer({ children }) {
  const [scrollY, setScrollY] = useState(0);

  return (
    <div onScroll={(e) => setScrollY(e.target.scrollTop)}>
      <p>目前滾動位置:{scrollY}</p>
      {children}
    </div>
  );
}

// 在外層組合
function App() {
  return (
    <ScrollContainer>
      {/* HeavyList 是在 App 中建立並作為 children 傳入 */}
      {/* 當 ScrollContainer 因為 scrollY 更新時,children 的引用未變,HeavyList 不會 Re-render! */}
      <HeavyList />
    </ScrollContainer>
  );
}

原理:HeavyList 是在 App 作用域下被創建的。當 ScrollContainer Re-render 時,它收到的 children 依然是上一次渲染時的同一份 React Element 物件,React 會自動跳過該子樹的重新渲染!


上一篇
Day 12 Component Render 生命週期:觸發 Re-render 的三大時機
下一篇
Day 14 Concurrent Mode 與 Fiber 架構:React 如何實現可中斷的渲染?
系列文
React 觀念架構:從js 基礎到Hook 底層邏輯 共 14 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言