【Next.js】ホットリロード(Fast Refresh)が効かない!?それ、メモ化が原因かも
こんにちは!
株式会社Sally 新人エンジニアの @haruten です♪
私たち株式会社Sallyでは、マーダーミステリーをスマホやPCで遊べるアプリ「ウズ」や、マーダーミステリーを制作してウズ上で公開・プレイできるエディターツール「ウズスタジオ」などを開発・運営しています。
弊社で複数のNext.jsプロジェクトを開発している中で、ホットリロード(以下、Fast Refresh)のパフォーマンスが大きく低下する問題が発生しました。
本記事では、その原因と対応策をまとめてご紹介します。
※ 本記事で扱う事象は、Next.js 15.3.3 + React 19.1.0 環境で確認されたものです。
起こっていた事象
弊社で開発・運用している複数の Next.js プロジェクトにおいて、Fast Refreshの挙動が明らかに重くなる現象が、慢性的に発生していました。
具体的には、以下のような現象が見られました。
-
UIやロジックに修正を加えるたびにフルリロードのような挙動が発生する
-
ページ全体が一瞬白くなり、状態がリセットされる
特に今回、新規モーダル UI の実装タスクにおいて、修正のたびにモーダルが閉じる事象が発生したことをきっかけに、問題の原因を調査することになりました。
原因の調査結果
結論として、Fast Refresh 時にフルリロードが発生していた原因は以下の 2 点でした。
-
Reactコンポーネント以外の複数のエクスポートが存在しているため
-
React.memo でメモ化したコンポーネントが再マウントされているため
Reactコンポーネント以外の複数のエクスポートが存在しているため
一部のページでFast Refresh時に以下のログが出力されていることを発見しました。
Fast Refresh had to perform a full reload when ./src/〜.tsx changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload
この原因は Next.js の公式ドキュメントにも記載されており、原因の特定は比較的スムーズでした。
The file you're editing might have other exports in addition to a React component.
React コンポーネント以外の値(関数や定数、型定義など)も同じファイルからエクスポートしている場合、Fast Refresh が正常に動作せず、フルリロードが発生する可能性があるとのことです。
また今回は該当しませんでしたが、以下のようなケースでもフルリロードが発生する可能性があるとされています。
- 無名関数として定義されたコンポーネント
- コンポーネント名がPascalCase ではなくcamelCase になっている
React.memo でメモ化したコンポーネントが再マウントされているため
通常、React.memo を使ってメモ化したコンポーネントは、props が変化しない限り再レンダリングをスキップできるため、パフォーマンス最適化の手段としてよく利用されます。
しかし、Fast Refresh の仕組み上、メモ化されたコンポーネントが正しく差分検知されないケースがあり、その結果として「再レンダリング」ではなく「再マウント」が発生してしまうことがあるようです。
特に弊社のプロダクトでは、アプリケーションのルートに近い階層のコンポーネントを React.memo でメモ化していたため、その影響が広範囲に及び、ページ内のほぼすべてのコンポーネントが再マウントされるという現象が発生していました。
結果としてフルリロードに近い現象が発生し、開発体験に大きな影響を与えていました。
メモ化が Fast Refresh の挙動を阻害するという報告は少なく、なぜ弊社プロダクトだけで起きているかは今回特定できませんでした。
とはいえ、同様の症状がある場合は、まずメモ化の影響を疑うことをおすすめします。
対策
Reactコンポーネント以外の複数のエクスポートが存在しているため
この点については、コンポーネントとそれ以外のロジック(定数・関数など)を別ファイルに切り出すことで対応しました。
また、再発防止のために eslint-plugin-react-refresh を ESLint に導入し、Fast Refresh に適さない export 構造を自動検出できるようにしました。
このプラグインは export のルールに関して警告を出してくれるため、開発中に問題のある書き方を事前にキャッチできるようになります。
React.memo でメモ化したコンポーネントが再マウントされているため
この問題に対してはいくつかの対処方法が考えられますが、今回は開発環境ではメモ化をスキップするカスタムのメモ化関数を用意し、それを使うことで対応しました。
const isDevServer = process.env.NODE_ENV === 'development' && typeof window !== 'undefined';
export const prodMemo = <P extends object>(component: FC<P>): FC<P> => {
if (isDevServer) {
return component;
}
const memoizedComponent = memo(component);
return memoizedComponent;
};
process.env.NODE_ENV をもとに開発環境かどうかを判定し、開発時はメモ化せずにそのまま返すことで、Fast Refresh が正常に動作するようにしました。
これにより、本番環境ではパフォーマンス最適化を維持しつつ、開発中の状態保持もしっかり確保できます。
この他にも、メモ化の対象をレンダリング負荷の高いコンポーネントに限定するなどの方法もあります。
将来的には React Compiler の正式リリースによって、こうした手動のメモ化対応自体が不要になると期待されます。
まとめ
今回の調査を通じて、同一ファイルでの複数のexportや、パフォーマンス最適化のために導入していたメモ化が Fast Refresh の体験を大きく損ねていたことが明らかになりました。
なお、今回ご紹介した問題はあくまで弊社プロダクトの環境に起因したケースであり、すべての環境で再現するとは限りません。
しかし、同様の違和感を抱えている方にとって、何かしらのヒントになれば幸いです。
Discussion