初めに
初めまして!
株式会社おてつたびというスタートアップでフルスタックエンジニアをしている、ぶりぼんこと橋本です。主にフロントエンド領域を開拓しており、ReactやTypeScriptが最も得意です。
おてつたびでは新しい旅の形を作るために、日夜開発に勤しんでいます。
Reactのstate管理は様々な種類があります。
React v16.8でリリースされたuseState
やuseReducer
、グローバルなstate管理を実現するredux
、最近流行のRecoil
などなど、stateを管理する手法がありすぎると言っても過言ではありません。
state管理はReactにおける醍醐味の一つであり、状況によってどのツールを使うかを判断していく必要があります。
本記事では、パフォーマンスという観点から、Reactのstate管理を述べたいと思います。
今回対象とするものは、useState
、useReducer
、redux
の3つです。
Reactのstate管理と言ったら上述の3つを使うことが基本になるかと思いますので、これらに焦点を当てていきます。
では、早速計測をしていきましょう!
計測準備
まず初めに、どのような機能で計測を行うかを説明します。
複雑な機能だとパフォーマンス計測をしづらいので、ボタンをクリックすると数値がインクリメントされるカウンター機能で計測を行います。
計測に用いるツールは、performance.now()
を使用します。
Date APIを使ったパフォーマンス計測の場合、ミリ秒単位に誤差が発生し、非常にパフォーマンス計測がしづらいです。
Performance APIはDate APIと同様、ブラウザで使用することができコンソール出力が容易に行えます。また、パフォーマンス計測のためのAPIであるため、Date APIと比べて正確性が担保されます。
以下の関数を用意して、callbackで計測対象の関数を受け取ります。
export const measure = (callback: Function, fixed: number = 5): void => {
const start = performance.now();
callback();
const end = performance.now();
const elapsed = end - start;
console.log(`実行時間: ${elapsed.toFixed(fixed)}`);
console.log(`start: ${start}`);
console.log(`end: ${end}`);
};
計測
useStateの計測
以下のコードでuseStateのパフォーマンスを計測します。
import React, { memo, useState } from 'react';
import { measure } from 'utils/measure';
interface Props {}
export const UseStatePage = memo((props: Props) => {
const [count, setCount] = useState(0);
const onClick = () => {
measure(() => setCount(prevState => prevState + 1));
};
return (
<div>
<p>{count}</p>
<button onClick={onClick}>click!</button>
</div>
);
});
動作環境や状況にもよりますが、私の環境では以下のような結果になりました。
全て1msを切り、平均でも0.28msとかなり良いパフォーマンスを発揮しています。
useReducerの計測
以下のコードでuseReducerのパフォーマンスを計測します。
/**
*
* UserReducerPage
*
*/
import React, { memo, useReducer } from 'react';
import { measure } from 'utils/measure';
interface State {
count: number;
}
const initialState: State = {
count: 0,
};
const reducer = (state, action) => {
switch (action.type) {
case 'setCount':
return { ...state, count: state.count + 1 };
default:
return state;
}
};
interface Props {}
export const UseReducerPage = memo((props: Props) => {
const [state, dispatch] = useReducer(reducer, initialState);
const onClick = () => {
measure(() => dispatch({ type: 'setCount' }));
};
return (
<div>
<p>{state.count}</p>
<button onClick={onClick}>click!</button>
</div>
);
});
useState同様、全て1msを切っています。
平均で0.16msと非常に良いパフォーマンスを発揮しています。誤差の範囲で再度計測した場合に結果が変わると思いますが、useStateより良いパフォーマンスとなっています。
reduxの計測
reduxのパフォーマンス計測に用いるコードですが、reduxjs/toolkitを用いて実装します。
reduxの公式でもreduxjs/toolkitを使用することを推奨しており、実際にtoolkitを使用することでredux実装がかなり早くなります。
以下がコードです(Reactのboilerplateで作成しており、一部コードを端折っています)。
/**
*
* ReduxPage
*
*/
import React, { memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { measure } from 'utils/measure';
import { useCountSlice } from './slice';
import { selectCount } from './slice/selectors';
interface Props {}
export const ReduxPage = memo((props: Props) => {
const { actions } = useCountSlice();
const dispatch = useDispatch();
const count = useSelector(state => selectCount(state).count);
const onClick = () => {
measure(() => {
dispatch(actions.setCount());
});
};
return (
<div>
<p>{count}</p>
<button onClick={onClick}>click!</button>
</div>
);
});
import { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from 'utils/@reduxjs/toolkit';
import { useInjectReducer, useInjectSaga } from 'utils/redux-injectors';
import { countSaga } from './saga';
import { CountState } from './types';
export const initialState: CountState = {
count: 0,
};
const slice = createSlice({
name: 'count',
initialState,
reducers: {
setCount(state, action: PayloadAction) {
state.count = state.count + 1;
},
},
});
export const { actions: countActions } = slice;
export const useCountSlice = () => {
useInjectReducer({ key: slice.name, reducer: slice.reducer });
useInjectSaga({ key: slice.name, saga: countSaga });
return { actions: slice.actions };
};
/**
* Example Usage:
*
* export function MyComponentNeedingThisSlice() {
* const { actions } = useCountSlice();
*
* const onButtonClick = (evt) => {
* dispatch(actions.someAction());
* };
* }
*/
半分以上で1msを越えており、平均も1msを越える結果となりました。
redux/toolkitを使っていたり、useStateやuseReducerと比べて無駄なコードがあると言っても、パフォーマンスとしてはあまり良くないと言えるのではないでしょうか。
まとめ
今回の記事で計測したソースコードは、あくまでも記事を書くためのものでしかないです。
それぞれの環境により異なった結果になる可能性はありますし、実用的で複雑なロジックの場合には全く逆の結果が生まれる可能性もあります。
(私自身パフォーマンス計測はここ数ヶ月で行い始めたもので、まだまだ専門的に見解を述べることができるほど、知見や経験が溜まっていません。)
しかし、useStateやuseReducerのパフォーマンスの良さを利用しない手はないでしょう。
今回の計測で私自身驚いたことは、useState
とuseReducer
でパフォーマンスにそれほど差がないということです。
フロントの設計の観点と今回のパフォーマンスの観点から、useReducer
はページ単位で複数のstateを管理する際に最適なものではないでしょうか。
次の記事では、追加で以下を検証しようと思います。
-
node.js
xExpress
で簡易的なサーバーをローカルで作成し、APIへリクエストする関数でパフォーマンス計測する - 上記サーバーをHerokuにデプロイして、APIへリクエストする関数でパフォーマンス計測する
また、設計的な観点からもstate管理を述べる記事も作成して、どの場合にどの機能を使えばいいかの私なりの結論を出せればと思います。
その記事を書き上げるまで、楽しみにお待ちいただければ嬉しいです。
APIリクエストを投げるとサーバー側の問題で計測に影響が出そうなので、何かいい方法があればコメントしていただけると嬉しいです!
最後に
[株式会社おてつたび](https://otetsutabi.com/?utm_source=qiita&utm_medium=social&utm_campaign= buri_tech_blog&utm_content=bottom)では、一緒に働いてくれる新しい仲間を探しています!
フロントエンドはReactを、バックエンドはRuby on Railsを使っています。
開発メンバー全員がフルスタックエンジニアとしてコードを書いていますが、各々が得意なスキルを発揮しているようなチームです。
少しでも気になるという方は、コメントいただけると嬉しいです!
最後までご覧いただき、ありがとうございました。