1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

useStateの初期値計算とパフォーマンス改善

Last updated at Posted at 2024-07-15

要旨

React のuseStateは引数にstateの初期値をとりますが、関数を渡す事もできます。
関数を渡した場合には、stateが初期化されるときにのみ関数が実行されて、初期値が渡されます。
レンダーのたびに初期化関数が呼び出されないため、計算量が大きい場合にはパフォーマンスの改善につながります。

この記事では、初期値の渡し方の異なる2つのコンポーネントの例をまじえながら動きを確認してみたいと思います。

useState とstateの初期値

React ではstateと呼ばれる変数をコンポーネントに紐づけて状態を管理できます。

このstateuseStateというhooks によってstate変更用の関数(set関数)とともに返されます。

const [state, setState] = useState(initialValue)

useStateは引数として受け取った値をstateの初期値とし、初回のレンダー時のみその値をstateに格納します。
stateが返された後にuseStateの引数の値を変更しても、元のstateには変更が入りません。)

計算量の大きい初期値

useStateに渡す初期値として、ある程度計算をしてから渡すこともあるかと思います。

初期値の計算量が多い、以下のようなコンポーネントを考えてみます。

import { useState } from "react";

const initializeState = () => {
  const expensivelyProcessedList: number[] = [];
  for (let i = 0; i < 10000; i++) {
    expensivelyProcessedList.push(i);
    expensivelyProcessedList.sort();
    expensivelyProcessedList.reverse();
  }
  console.log("非最適化コンポーネントでinitializeState が呼び出されました");
  return [1, 2, 3];
};

export const NonOptimizedList = () => {
  const [state] = useState(initializeState());
  const [lang, setLang] = useState<"en" | "ja">("ja");

  return (
    <>
      <button
        onClick={() => {
          setLang((prevLang) => {
            return prevLang === "ja" ? "en" : "ja";
          });
        }}
      >
        {lang}
      </button>
      <ul>
        {state.map((element) => {
          return <li key={element}>{element}</li>;
        })}
      </ul>
    </>
  );
};

このコンポーネントでは、initializeStateという関数によって配列を無駄に何度も並び替える処理をしています。
計算量の大きい初期化用の関数initializeStateuseState(initializeState())のように実行する形でuseStateの初期値として渡しています。

useStateに渡した初期値はReact によって初期化時以外には呼び出されたあとで破棄されます。
そのため、重たい処理によって初期値を計算してから渡してしまうと、重い処理が何度も呼び出されることになってしまいます。


上のコンポーネントには言語表示を切り替えるボタン(enjaと書かれたボタン)がつけられています。

このボタンを押した際にもReact のレンダーによってこのコンポーネントが呼び出されるためinitializeState()が実行されてしまっています。
(コンソール上には"非最適化コンポーネントでinitializeState が呼び出されました"がボタンを押すたびに表示されます。)

そのため、ボタンの表示を変えるだけの処理にも時間がかかってしまっています。

初期化関数を初期値に渡す

useStateの初期値には初期化用の関数を渡すことができます。
関数を初期値として渡した場合には、React はuseState初期化を行うときにのみ関数を実行して、その結果を初期値として渡してくれるようになります。


先ほどのコンポーネント例を以下のように書き換えてみます。

import { useState } from "react";

const initializeState = () => {
  const expensivelyProcessedList: number[] = [];
  for (let i = 0; i < 10000; i++) {
    expensivelyProcessedList.push(i);
    expensivelyProcessedList.sort();
    expensivelyProcessedList.reverse();
  }
  console.log("最適化コンポーネントでinitializeState が呼び出されました");
  return [3, 2, 1];
};

export const OptimizedList = () => {
  const [state] = useState(initializeState);
  const [lang, setLang] = useState<"en" | "ja">("ja");

  return (
    <>
      <button
        onClick={() => {
          setLang((prevLang) => {
            return prevLang === "ja" ? "en" : "ja";
          });
        }}
      >
        {lang}
      </button>
      <ul>
        {state.map((element) => {
          return <li key={element}>{element}</li>;
        })}
      </ul>
    </>
  );
};

先の例では関数実行後の値をuseState(initializeState())という形で渡していましたが、この例では関数を直接渡す形useState(initializeState)となっています。


initializeStateは実行されると「最適化コンポーネントでinitializeState が呼び出されました」というメッセージがコンソール上に表示されるようになっています。

しかし、このコンポーネントの言語切り替えボタンを押してみてもコンソール上に新たにメッセージが表示されることはなく、画面への反映もスムーズに行われていることが確認できます。

パフォーマンス比較

React Developer Tools のProfile タブで言語切り替えボタンを押した際に起こるレンダーにかかった時間をそれぞれ計測してみました。

useStateに直接値を渡しているNonOptimizedListの方ではRender Duration が約1,000ms かかっているのに対し、初期化関数を渡しているOptimizedListの方では0.2 ms ~ 0.3ms という結果となりました。

初期値の計算にとても時間がかかるような場合には、初期値の渡し方一つでここまでパフォーマンスが変わることがあるわけですね。

今回扱ったコード例は以下に置いてあります。

さいごに

React で計算量を抑える方法としてはuseMemouseCallbackの使用などもありますが、useStateへの初期値の渡し方一つとってみてもパフォーマンスに大きな違いが出うるということは意識しておきたいですね。

単純に値を渡すのではなく関数自体を渡すようなコードとなるため、場合によっては可読性が低下することがあるかもしれません。
最適化によるパフォーマンス改善の効果とコードの可読性低下の度合いはトレードオフの関係となることも多いため、バランスを考えて採用するかどうかを検討したいですね。

余談: React Compiler

useMemouseCallbackについては、React 19 以降、React Compiler によって自動で最適化できるようになるみたいですね。

このコンパイラは、JavaScript と React のルールに関する知識を使用して、コンポーネントやフック内にある値や値のグループを、自動的にメモ化します。1

  1. React Compiler のドキュメントは執筆時点では暫定版であり、内容が変わる場合があります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?