82
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Reactアプリケーションを高速化するための方法

Last updated at Posted at 2021-06-21

Reactアプリケーションを高速化するための方法

結論から

無駄な再レンダリングを発生させない

処理しないこと

高速化とは処理しないことです
矛盾しているように思えますが、処理が減ればアプリケーションは高速化します

Reactの処理を減らすためには

再レンダリングをしないことです
もちろん完全に再レンダリング無しというのは不可能なので、いかにそれを減らしていくかが重要です

再レンダリングの妥当性

妥当な再レンダリング

コンポーネントのstateに変化があって自身の挙動を変更しなければならないとしたら、その再レンダリングは妥当です

妥当ではない再レンダリング

コンポーネント自身がそのstateを必要としておらず、下位に配るためstateを保持しているとしたら、その際レンダリングは妥当ではありません
そういう構造を作ってしまえば、配下にいる全コンポーネントがstateと無関係に再レンダリングされます。

何故、こんな構造がまかり通ってしまうのか

useStateが三つの機能を一つに集約してしまったのが原因です。詳しくは[React]useStateによるstate管理の問題点とその解決方法を模索した結果、3つの機能を分離するに至った話を参照してください。

解決方法

上位コンポーネントで下位に配るためのstateを保持しない

無駄なstateの保持、これが諸悪の根源です
とにかく自身で使用しないstateを保持するのをやめる必要があります

上位コンポーネントでstateを保持しないためには

state管理ライブラリを使用する

ReduxやContextAPIを使用すれば、親コンポーネントを経由せずともstateを配ることが出来ます
ただし、以下の欠点があります

  • stateのスコープがグローバルになってしまい、特定のコンポーネントグループ内で閉じさせるのが困難
  • 仕組みを作成するコストが大きく、気軽に利用しにくい

state管理をコンポーネントグループでローカル化する

特定コンポーネント間で閉じられたstate管理が出来て、簡単に利用出来れば問題は解決です
ということで作りました

両方とも、stateやeventを扱うための識別ハンドルを上位コンポーネントで作成し、それを下位コンポーネントで使用する仕組みです
ハンドルを作成した上位コンポーネントはstateの更新やevent通知の影響を受けないので、再レンダリングはされません

state管理のグループローカル化の具体例

Sample1 特に対策を打たない、通常の書き方

Inputコンポーネントにボタンが配置されており、押すごとにstateが+1されます
Test1とTest2はstateを受け取って、値を1万回表示します
Test3はstateとは無関係にTest3を1万回表示します

import React, { useState } from 'react';

const count = 10000;

const Test1 = ({ value }: { value: number }) => (
  <div>
    <div>Test1</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
);
const Test2 = ({ value }: { value: number }) => (
  <div>
    <div>Test2</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
);
const Test3 = () => (
  <div>
    <div>Test3</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>Test3</div>
    ))}
  </div>
);
const Input = ({ setValue }: { setValue: React.Dispatch<React.SetStateAction<number>> }) => (
  <button onClick={() => setValue((v) => v + 1)}>+1</button>
);

const App = () => {
  const [value, setValue] = useState(0);
  return (
    <>
      <div>通常</div>
      <Input setValue={setValue} />
      <hr />
      <div style={{ display: 'flex' }}>
        <Test1 value={value} />
        <Test2 value={value} />
        <Test3 />
      </div>
    </>
  );
};
export default App;

Sample2 memo化

Sample1をmemo化したものです

import React, { memo, useState } from 'react';

const count = 10000;

const Test1 = memo(({ value }: { value: number }) => (
  <div>
    <div>Test1</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
));
const Test2 = memo(({ value }: { value: number }) => (
  <div>
    <div>Test2</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
));
const Test3 = memo(() => (
  <div>
    <div>Test3</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>Test3</div>
    ))}
  </div>
));
const Input = memo(({ setValue }: { setValue: React.Dispatch<React.SetStateAction<number>> }) => (
  <button onClick={() => setValue((v) => v + 1)}>+1</button>
));

const App = () => {
  const [value, setValue] = useState(0);
  return (
    <>
      <div>memo化</div>
      <Input setValue={setValue} />
      <hr />
      <div style={{ display: 'flex' }}>
        <Test1 value={value} />
        <Test2 value={value} />
        <Test3 />
      </div>
    </>
  );
};
export default App;

Sample3 グループローカル化

useLocalStateによって、上位コンポーネントの再レンダリングを行わない書き方になっています

import {
  LocalState,
  mutateLocalState,
  useLocalState,
  useLocalStateCreate,
} from '@react-libraries/use-local-state';
import React from 'react';

const count = 10000;

const Test1 = ({ state }: { state: LocalState<number> }) => {
  const [value] = useLocalState(state);
  return (
    <div>
      <div>Test1</div>
      {new Array(count).fill(0).map((_, index) => (
        <div key={index}>{value}</div>
      ))}
    </div>
  );
};
const Test2 = ({ state }: { state: LocalState<number> }) => {
  const [value] = useLocalState(state);
  return (
    <div>
      <div>Test2</div>
      {new Array(count).fill(0).map((_, index) => (
        <div key={index}>{value}</div>
      ))}
    </div>
  );
};
const Test3 = () => (
  <div>
    <div>Test3</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>Test3</div>
    ))}
  </div>
);
const Input = ({ state }: { state: LocalState<number> }) => (
  <button onClick={() => mutateLocalState(state, (v) => v + 1)}>+1</button>
);

const App = () => {
  const state = useLocalStateCreate(0);
  return (
    <>
      <div>localState</div>
      <Input state={state} />
      <hr />
      <div style={{ display: 'flex' }}>
        <Test1 state={state} />
        <Test2 state={state} />
        <Test3 />
      </div>
    </>
  );
};
export default App;

結果の検証

ボタンを押してstateを変化させたときのprofile結果です
Render durationがレンダリング時間です

Sample1の結果

レンダリング時間 228.2ms

特に何も対応していないので、全てのコンポーネントが再レンダリングされています
当然、一番遅いです

image.png

Sample2の結果

レンダリング時間 162.2ms

memo化によってTest3の更新はスキップされていますが、Appは再レンダリングされています
何もしないよりは速度が向上していますが、まだ無駄が残っています

image.png

Sample3の結果

レンダリング時間 95.2ms

本当に更新が必要なコンポーネントのみ再レンダリングされています
処理を行う内容は必要最低限です

image.png

まとめ

Reactアプリケーションを高速化させるためには、更新の必要が無いコンポーネントを再レンダリングしないことです。そのためにはとにかく上位コンポーネントに余計なstateを持たせないというのが基本です

今回は効率的なstate管理のために、stateをグループで閉じるためのライブラリを使用しました。その他にstateを使わずにevventのみ配るライブラリもあるので、次回はそちらも紹介したいと思います

82
84
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
82
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?