Next.jsでReact Compilerを試してみた

以下を実行して、Nextjs プロジェクトを作成。React Compiler を使用するには、React 19 に対応している必要があるため、canary バージョンで作成する。
npx create-next-app@canary
ちなみに、14 から 15 へアップデートする場合は、以下を実行
npm i next@rc react@rc react-dom@rc eslint-config-next@rc
※ Typescript を使用しているのであれば、@types/react と @types/react-dom も最新バージョンにアップアップデートする必要がある。

Next.js 15 RC のブログに書いていたとおり、初期画面が新しくなってる

以下のような 2つのコンポーネントを作成。(超適当)
Child コンポーネントはメモ化しておく
'use client'
import React, { useState, useMemo, useCallback } from 'react'
import { Child } from './Child'
const heavyComputation = (num: number) => {
console.log('Heavy computation in progress...')
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += num;
}
return result;
}
export const Parent = () => {
console.log('Rendering Parent')
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const calcResult = heavyComputation(count)
const handleIncrement = () => {
setCount((count) => count + 1)
}
const handleToggle = () => {
setFlag(!flag)
}
return (
<div>
<p>Count: {count}</p>
<Child handleIncrement={handleIncrement} />
<p>Calculation Result: {calcResult}</p>
<button type="button" onClick={handleToggle}>Toggle Flag</button>
<p>Flag: {flag ? 'True' : 'False'}</p>
</div>
)
}
import React from 'react'
type ChildProps = {
handleIncrement: () => void
};
export const Child = React.memo(({ handleIncrement }: ChildProps) => {
console.log('Rendering Child')
return (
<>
<button type="button" onClick={handleIncrement}>+1</button>
</>
)
})
Child.displayName = 'Child'

Toggle ボタンを押すと、calcResult が再計算されるし、Parentコンポーネントがレンダリングされるたびに handleIncrement関数が再生成されるので、 それを Props として受け取る Child コンポーネントも再レンダリングされる。

useMemo と useCallback を利用して、calcResult と handleIncrement をそれぞれメモ化。
const calcResult = useMemo(() => heavyComputation(count), [count])
const handleIncrement = useCallback(() => {
setCount((count) => count + 1);
}, [])

メモ化したことで、Toggle ボタンを押しても、Parent コンポーネントが再レンダリングされるのみ。

React Compiler のインストール
npm install babel-plugin-react-compiler

手動のメモ化は排除する
'use client'
import React, { useState, useMemo, useCallback } from 'react'
import { Child } from './Child'
const heavyComputation = (num: number) => {
console.log('Heavy computation in progress...')
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += num;
}
return result;
}
export const Parent = () => {
console.log('Rendering Parent')
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const calcResult = heavyComputation(count)
const handleIncrement = () => {
setCount((count) => count + 1)
}
const handleToggle = () => {
setFlag(!flag)
}
return (
<div>
<p>Count: {count}</p>
<Child handleIncrement={handleIncrement} />
<p>Calculation Result: {calcResult}</p>
<button type="button" onClick={handleToggle}>Toggle Flag</button>
<p>Flag: {flag ? 'True' : 'False'}</p>
</div>
)
}
import React from 'react'
type ChildProps = {
handleIncrement: () => void;
}
export const Child = ({ handleIncrement }: ChildProps) => {
console.log('Rendering Child')
return (
<>
<button type="button" onClick={handleIncrement}>+1</button>
</>
)
}

Toggle ボタンを押しても、calcResult の再計算とChild コンポーネントの再レンダリングは行われない。

React Compiler 神

object もメモ化されるのか試してみた。
useEffect の依存配列に object を指定することは基本ないと思うが、以下のように object を useEffect の依存配列に含める。
const object = {aa: 'aa', bb: 'bb'}
useEffect(() => {
console.log('')
}, [object])
本来、object をメモ化していないので、コンポーネントがレンダリングされるたびに、console が実行されるはずだが、実行されなかったので、React Compiler が有効なとき、オブジェクトも自動的にメモ化される。

React Compiler を使用することで、手動のメモ化が必要なくなり、自動的にメモ化されるようになるのはほんまに魅力しかないな。可読性も間違いなく上がって、シンプルで直感的なコードになる。
React の勉強始めたての時に一番つまずいたのがメモ化の部分(useMemo と useCallback の概念)やったから、初学者にとって学習のハードルが下がるんじゃないかな。