こんにちは。tampopo256です。
普段は教育系のWebサービスを作ったりしています。
なぜこの記事を書こうと思ったか
「この画面、なんかカクつく…」
ある日のプロダクトレビューで、非エンジニアのメンバーからそんな指摘を受けました。開発者的には「特に重い処理はしてないはず」と思っていたのですが、調べてみると、無駄な再レンダリングや非効率なstate構造が原因でした。
Reactの最適化って、「書けるようになった」あとの第二の壁なんですよね。今回は、自分がReact開発の中で直面したパフォーマンス問題と、その対処法を共有します。
背景と前提
-
使用技術: React 18, TypeScript, React Router, Zustand, Chakra UI
-
アプリ構成: 複数のダッシュボード画面があるSPA、APIから大量データを取得して描画
-
主な症状:
- ページ遷移後の表示がワンテンポ遅れる
- スクロール中に動作がカクつく
- モーダルを開くとCPU使用率が跳ね上がる
試行錯誤の過程とTips
Tip 1. useMemoとuseCallbackは「なんとなく」使わない
やってしまいがちなのが、パフォーマンス改善のためにuseMemo
やuseCallback
を多用するパターン。
const handleClick = useCallback(() => {
doSomething();
}, []);
しかし、不必要なメモ化は逆効果になることも。メモリ消費が増えたり、可読性が下がったりします。
🔧 対策:
- メモ化は コストが高い処理 or 再レンダリングで生成されるpropsを渡す時のみ
- eslint-plugin-react-hooksとprofilerでボトルネックを確認してから導入
Tip 2. React.memoを活用して「無駄な再描画」を防ぐ
子コンポーネントが毎回再レンダリングされる問題、ありますよね。
特に以下のように親のstate更新が頻繁な時。
<HeavyComponent data={data} />
🔧 対策:
const HeavyComponent = React.memo(({ data }) => {
return <div>{data.name}</div>;
});
React.memoの効果を正しく使うことで、再描画コストを大幅に削減できます。
Tip 3. 状態管理は細かく分離しよう(Context肥大化は危険)
React Contextは便利ですが、グローバルに置きすぎると変更時に全体が再レンダリングされがち。
🔧 対策:
- 必要に応じてZustandやRecoilなど、状態単位で分離できるライブラリを併用
- Contextは「設定」や「認証情報」など、更新頻度の低いデータに絞る
Tip 4. リスト表示はvirtualizeするのが前提
array.map()
で100件以上のリストをそのまま描画すると、当然遅いです。
🔧 対策:
-
react-window
やreact-virtualized
を活用して仮想化 - 不要なDOM描画を極力減らすことで初期表示が爆速に
Tip 5. useTransitionで非同期UIをスムーズに
React 18のuseTransition
を使えば、「重要ではない処理」を後回しにできます。
🔧 対策例:
const [isPending, startTransition] = useTransition();
startTransition(() => {
setState(nextState); // 画面の描画に影響しないもの
});
「ちょっと遅れてもいい処理」をうまく分離することで、ユーザーの体感速度が大きく改善しました。
気付きと教訓(本質)
パフォーマンス改善って、「書き方を覚えた」段階では見えない問題にぶつかって初めて真価が問われる工程だと思います。
- どこが重いのか?
- なぜ重いのか?
- それは最適化すべき対象なのか?
この問いを立ててから初めて、「改善」ではなく「設計の再考」が始まります。
まとめ
以下、Reactアプリのパフォーマンス改善で特に役立ったTipsをまとめます:
- useMemo/useCallbackの乱用は逆効果になることも
- React.memoで不要な再レンダリングを防ぐ
- Contextを万能薬にせず、細かく分離した状態管理を
- 長いリストは仮想スクロールで最適化
- useTransitionで非同期UIの体感速度を上げる