【超便利】React Compilerで楽にメモ化をしよう
はじめに
こんにちは!駆け出しエンジニアのしっしーです!
株式会社HARVESTでフロントエンドエンジニアをやっております!
弊社では React をメインに使ってフロントエンドを開発していますが、そのぶん「パフォーマンスチューニングどうするか問題」といつも向き合っています。
特にコンポーネントの再レンダー最適化は、useMemo や useCallback、React.memo などをどこまで書くべきか判断が難しく、「とりあえず付けておくか…」となりがちですよね。
そこで今回は、そうした最適化の悩みをかなり軽くしてくれる React Compiler について、「何がうれしいのか」「どうやって導入するのか」を、自分の学びも兼ねてまとめてみました。
React Compilerとは
React Compiler は、ビルド時にアプリを自動で最適化してくれる新しいツールです。
今まで手動で書いていた useMemo や useCallback、React.memo をコンパイラが勝手にやってくれるので、開発者はもう面倒なmemo化を気にしなくてよくなります。(最高)
インストール
インストールは以下のコマンドになります。
npm install -D babel-plugin-react-compiler@latest
設定方法
React + Vite の場合
Viteの設定ファイルに、以下のように追加
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
react({
babel: {
plugins: [["babel-plugin-react-compiler", {}]],
},
}),
],
});
Next.jsの場合
Next.jsの設定ファイルに、以下のように追加
import { NextConfig } from "next";
const nextConfig: NextConfig = {
reactCompiler: false,
};
export default nextConfig;
Next.jsがv16未満の場合は、以下のようにexperimentalでネストする
import { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
reactCompiler: false,
},
};
export default nextConfig;
サンプルコード(React + Vite)
memo化したコンポーネントと、非memo化コンポーネントの動作比較をするためのサンプルコードです。
サンプルは React + Vite 向けに書いていますが、やっていること自体は Next.js でもまったく同じなので、「親コンポーネント」と「子コンポーネント」の関係だけ見てもらえれば OK です。
import { useCallback, useState } from "react";
import ChildSample from "./components/ChildSample";
import MemorizedChildSample from "./components/MemorizedChildSample";
export default function SamplePage() {
const [values, setValues] = useState({ valueA: 0, valueB: 0 });
const [counter, setCounter] = useState(0);
const handleSetValueA = useCallback((value: number) => {
setValues((prev) => ({ ...prev, valueA: value }));
}, []);
const handleSetValueB = (value: number) => {
setValues((prev) => ({ ...prev, valueB: value }));
};
const handleIncrement = () => {
setCounter((prev) => prev + 1);
};
return (
<div className="flex flex-col gap-10">
<button onClick={handleIncrement}>親を再レンダリング(カウンター: {counter})</button>
<h1>MemorizedChildSample(memo化したコンポーネント)</h1>
<MemorizedChildSample value={values.valueA} setValue={handleSetValueA} />
<h1>ChildSample(非memo化コンポーネント)</h1>
<ChildSample value={values.valueB} setValue={handleSetValueB} />
</div>
);
}
memo化したコンポーネント
import { memo } from "react";
interface MemorizedChildSampleProps {
value: number;
setValue: (value: number) => void;
}
export default memo(function MemorizedChildSample(props: MemorizedChildSampleProps) {
return (
<input
type="number"
className="rounded-md border border-gray-300 p-2"
value={props.value}
onChange={(event) => props.setValue(Number(event.target.value))}
/>
);
});
memo化されていないコンポーネント
interface ChildSampleProps {
value: number;
setValue: (value: number) => void;
}
export default function ChildSample(props: ChildSampleProps) {
return (
<input
type="number"
className="rounded-md border border-gray-300 p-2"
value={props.value}
onChange={(event) => props.setValue(Number(event.target.value))}
/>
);
}
動作の違い
React Developer Tools を使って、想定どおりに最適化されているかを確認していきます。
理想の状態
- 親コンポーネント(
App.tsx)の状態が変化して再レンダーしても、非 memo 化コンポーネント(ChildSample)が再レンダーされない - memo 化したコンポーネント(
MemorizedChildSample)の状態が変化しても、非 memo 化コンポーネント(ChildSample)が再レンダーされない
React Compiler なし
親コンポーネント(App.tsx)の状態が変化して再レンダーされたり、memo 化したコンポーネント(MemorizedChildSample)の状態が変化して再レンダーされたりすると、非 memo 化コンポーネント(ChildSample)も一緒に再レンダーされてしまいます。

React Compiler あり
親コンポーネント(App.tsx)の状態が変化して再レンダーされたり、memo 化したコンポーネント(MemorizedChildSample)の状態が変化して再レンダーされたりしても、非 memo 化コンポーネント(ChildSample)は再レンダーされません!🤩

まとめ
React Compiler を使うと、これまで手作業で頑張っていた useMemo / useCallback / React.memo といった最適化の多くをコンパイラ側に任せられるようになります。
その結果、「どこまで最適化を書くべきか」を毎回悩む必要が減り、パフォーマンスをそこそこ保ちつつ、まずは機能開発に集中しやすくなるのが一番のメリットだと感じました。
今回紹介したように、設定自体もそこまで難しくないので、React や Next.js を使っている方は、ぜひ小さなプロジェクトからでも試してみてください!
参考文献
お知らせ
株式会社HARVESTでは、一緒に働くエンジニアを募集しております。
ご興味のある方は、recruit@harvest-w.com までご連絡下さい!
Discussion