React.js - 離開 vue 的我,和從前的 react 夥伴往 useState Lazy Initialization 邁進

在有需求時發現的新知識,總是特別難忘!

在 react 專案效能調整上,發現了最基礎的玩意。

 

 

前言

因爲工作使用 React 開發,對於許久沒碰的我來說,難免錯過了許多好料。

這篇的原由是有一次「質疑」現有專案的寫法:

const [list, setList] = useState(() => getList());
閱讀他人的代碼,先質疑,再理解。這也是一種成長的方式。

 

仔細查了,才發現原來這叫做「useState Lazy Initialization」。

回到最基礎的文件上,可以找到這篇:Avoiding recreating the initial state

它就是在講述使用useState,傳入function可以使狀態僅在初始時 render。

 

如果 function需要花費較高的運算時間,甚至引起畫面卡頓,那麼這是一個有效提升效能的做法。

中文應該叫做:惰性初始 state

 

這篇記錄學習內容,閱讀後你應該會知道 useState lazy initialization:

  1. 如何正確起手式。
  2. 概念與實踐方式。
  3. 有無使用的差異。
     

 

 

一、初始化方式的兩種模式

1. 直接傳遞初始值

const [todos, setTodos] = useState(createInitialTodos());
  • 初始值會在 組件每次渲染時 計算。
  • 如果createInitialTodos()運算較重,將會影響效能。

 

2. 傳遞函數進行 lazy initialization

const [todos, setTodos] = useState(() => createInitialTodos());
// or 
const [todos, setTodos] = useState(createInitialTodos);
  • React 只會在第一次渲染時執行該函數,並將結果作為初始狀態。
  • 避免在每次渲染時執行昂貴的計算。

 

3. 建立昂貴計算

嘗試將createInitialTodos弄得複雜一點,你會發現開始延遲了。

/**
 * @param {number} depth
 * @param {number} size
 */
function createInitialTodos(depth = 3, size = 100) {
  if (depth === 0) {
    return Array(size)
      .fill(0)
      .map((_, i) => i);
  }
  return Array(size)
    .fill(0)
    .map(() => createInitialTodos(depth - 1, size));
}

 

 

二、使用範例來比較效能

完整範例放在:https://github.com/explooosion/react-lazy-initialization

 

1. 初始 CRA 專案

使用一個乾淨的專案來測試。

npx create-react-app my-app

[ App.css ]

記得先移除所有樣式。

 

2. 建立兩種 Components 比較

沿用剛剛昂貴計算的function createInitialTodos()

 

建立一個直接傳遞初始值的 ComponentAppWithDirectValue

[ App.js ]

function AppWithDirectValue() {
  const [count, setCount] = useState(0);

  console.time("Direct initializer");
  const [todos, setTodos] = useState(createInitialTodos());
  console.timeEnd("Direct initializer");

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        AppWithDirectValue ({count})
      </button>
    </div>
  );
}
  • useState上下使用console.time, console.timeEnd記錄時間。
  • useState直接傳入值createInitialTodos
  • 使用一個button來觸發re-render

 

以及建立一個傳遞函數進行 lazy initialization 的 Component AppWithLazyInitializer

[ App.js ]

function AppWithLazyInitializer() {
  const [count, setCount] = useState(0);

  console.time("Lazy initializer");
  const [todos, setTodos] = useState(createInitialTodos);
  console.timeEnd("Lazy initializer");

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        AppWithLazyInitializer ({count})
      </button>
    </div>
  );
}
  • 同樣在useState上下使用console.time, console.timeEnd記錄時間。
  • useState傳入函數createInitialTodos,你也可以傳入() => createInitialTodos()
  • 使用一個button來觸發re-render

 

接著在 App 使用,就可以來比較囉!

[ App.js ]

export default function App() {
  return (
    <>
      <AppWithDirectValue />
      <AppWithLazyInitializer />
    </>
  );
}

 

完整代碼:

[ App.js ]

// @ts-check

import React, { useState } from "react";

/**
 * @param {number} depth
 * @param {number} size
 */
function createInitialTodos(depth = 3, size = 100) {
  if (depth === 0) {
    return Array(size)
      .fill(0)
      .map((_, i) => i);
  }
  return Array(size)
    .fill(0)
    .map(() => createInitialTodos(depth - 1, size));
}

function AppWithDirectValue() {
  const [count, setCount] = useState(0);

  console.time("Direct initializer");
  const [todos, setTodos] = useState(createInitialTodos());
  console.timeEnd("Direct initializer");

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        AppWithDirectValue ({count})
      </button>
    </div>
  );
}

function AppWithLazyInitializer() {
  const [count, setCount] = useState(0);

  console.time("Lazy initializer");
  const [todos, setTodos] = useState(createInitialTodos);
  console.timeEnd("Lazy initializer");

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        AppWithLazyInitializer ({count})
      </button>
    </div>
  );
}

export default function App() {
  return (
    <>
      <AppWithDirectValue />
      <AppWithLazyInitializer />
    </>
  );
}

 

 

三、Lazy Initialization 合適的場景

✅ 適用於初始值計算量大,例如:

  • 具有龐大的值或深度的資料。
  • 複雜計算,例如來源是解析 JSON、深拷貝物件。
  • 只需要在 第一次渲染 設定狀態的情境。
     

❌ 不適用於:

  • 簡單的靜態值,如 useState(0)useState(false)
  • 不影響效能的初始化(過度使用 Lazy Initialization 反而讓代碼變得難讀)。

 

 

reference

 

有勘誤之處,不吝指教。ob'_'ov