本文へスキップ
React Compiler

Part 8 · パフォーマンス

45 / 64

React Compiler

上の 3 つが、そのうち要らなくなる話

この Part で 3 つの道具を見てきました。memouseMemouseCallback

正直なところ、面倒だと思いませんでしたか。それは正しい感覚です。

React チームも同じことを考えました。 そしてこの 3 つを自動で書いてくれる仕組みを作りました。 それが React Compiler です。

何をしてくれるのか

React Compiler は、ビルドのときにコードを読んで、必要な場所に自動でメモ化を入れます

// 自分で書くコード
function Counter() {
  const [count, setCount] = useState(0);
  const handleSave = () => save(count);

  return <Child onSave={handleSave} />;
}

// コンパイラが変換したあと(イメージ)
// handleSave は count が変わったときだけ作り直される

つまり、この Part でやってきたことを、書かなくてよくなりますuseCallbackuseMemomemo も、自分で書く必要がなくなります。

with-callback.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";

// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
  return (
    <RenderBox title="memo した子" tone="highlight">
      <Button size="sm" variant="outline" onClick={onSave}>
        保存
      </Button>
    </RenderBox>
  );
});

export function WithCallback() {
  useTrackDemoRender();

  const [count, setCount] = useState(0);
  const [saved, setSaved] = useState(0);

  // 同じ関数を使い回す。依存配列が空なので、作り直されることがない
  const handleSave = useCallback(() => setSaved((s) => s + 1), []);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">
        count: {count} / 保存した回数: {saved}
      </p>

      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count を増やす(子には関係ない)
      </Button>

      <Child onSave={handleSave} />
    </div>
  );
}

では、この 3 章は無駄だったのか

まったく無駄ではありません。理由は 3 つあります。

1. まだ全部のプロジェクトで使えるわけではない

導入されていないプロジェクトはたくさんあります。 既存のコードを読むときにも、memo useCallback は出てきます。読めなければ話になりません。

2. 効かない場所がある

コンパイラは、React のルールを守って書かれたコードにしか適用されません。 レンダリング中に値を書き換えているような、ルール違反のコードは対象外になります。

「なぜかコンパイラが効いていない」となったとき、 原因を探せるかどうかは、この 3 章を理解しているかで決まります。

3. 何が起きているか分からないまま使うことになる

自動化された仕組みは、うまく動いている間は考えなくて済みます。 問題はうまく動かなかったときです。

with-callback.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";

// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
  return (
    <RenderBox title="memo した子" tone="highlight">
      <Button size="sm" variant="outline" onClick={onSave}>
        保存
      </Button>
    </RenderBox>
  );
});

export function WithCallback() {
  useTrackDemoRender();

  const [count, setCount] = useState(0);
  const [saved, setSaved] = useState(0);

  // 同じ関数を使い回す。依存配列が空なので、作り直されることがない
  const handleSave = useCallback(() => setSaved((s) => s + 1), []);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">
        count: {count} / 保存した回数: {saved}
      </p>

      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count を増やす(子には関係ない)
      </Button>

      <Child onSave={handleSave} />
    </div>
  );
}

使ってみるには

# コンパイラ本体を入れて
bun add -D babel-plugin-react-compiler
// next.config.ts — 設定はこの 1 行
const nextConfig: NextConfig = {
  reactCompiler: true,
};

React Compiler は 1.0 で安定版になり、 Next.js 16 では設定項目から experimental が取れました。もう実験的な機能ではありません。 (古い記事にある experimental: { reactCompiler: true } は Next.js 15 までの書き方です)

導入したら、既存の useMemo useCallback は、動作を確かめながら少しずつ外していけます。 一度に全部消さないでください。 さきほど見たとおり、コンパイラはReact の決まりを守れていない場所では手を出しません。 そこだけは自分のメモ化が効いていた、ということがあり得ます。

with-callback.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";

// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
  return (
    <RenderBox title="memo した子" tone="highlight">
      <Button size="sm" variant="outline" onClick={onSave}>
        保存
      </Button>
    </RenderBox>
  );
});

export function WithCallback() {
  useTrackDemoRender();

  const [count, setCount] = useState(0);
  const [saved, setSaved] = useState(0);

  // 同じ関数を使い回す。依存配列が空なので、作り直されることがない
  const handleSave = useCallback(() => setSaved((s) => s + 1), []);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">
        count: {count} / 保存した回数: {saved}
      </p>

      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count を増やす(子には関係ない)
      </Button>

      <Child onSave={handleSave} />
    </div>
  );
}

理解できたか確かめる

確認クイズ

React Compiler を導入すると何が変わる?

確認クイズ

Compiler があるなら、memo や useCallback は学ばなくてよい?

with-callback.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";

// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
  return (
    <RenderBox title="memo した子" tone="highlight">
      <Button size="sm" variant="outline" onClick={onSave}>
        保存
      </Button>
    </RenderBox>
  );
});

export function WithCallback() {
  useTrackDemoRender();

  const [count, setCount] = useState(0);
  const [saved, setSaved] = useState(0);

  // 同じ関数を使い回す。依存配列が空なので、作り直されることがない
  const handleSave = useCallback(() => setSaved((s) => s + 1), []);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">
        count: {count} / 保存した回数: {saved}
      </p>

      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count を増やす(子には関係ない)
      </Button>

      <Child onSave={handleSave} />
    </div>
  );
}

この章のまとめ

  • React Compiler は、メモ化を自動で入れてくれる仕組み
  • 人間の勘より機械的なので、包み忘れも包みすぎも起きない
  • ただし既存コードを読むとき効かないときの原因究明には、手で書く知識が要る
  • 道具が変わっても、「React は同じものかどうかだけを見る」という仕組みは変わらない

with-callback.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";

// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
  return (
    <RenderBox title="memo した子" tone="highlight">
      <Button size="sm" variant="outline" onClick={onSave}>
        保存
      </Button>
    </RenderBox>
  );
});

export function WithCallback() {
  useTrackDemoRender();

  const [count, setCount] = useState(0);
  const [saved, setSaved] = useState(0);

  // 同じ関数を使い回す。依存配列が空なので、作り直されることがない
  const handleSave = useCallback(() => setSaved((s) => s + 1), []);

  return (
    <div className="flex flex-col gap-4">
      <p className="rounded-md border p-3 font-mono">
        count: {count} / 保存した回数: {saved}
      </p>

      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count を増やす(子には関係ない)
      </Button>

      <Child onSave={handleSave} />
    </div>
  );
}