月曜日, 5月 12, 2025
ホームニューステックニュースReact Compilerを有効にして9ヶ月が経ちました

React Compilerを有効にして9ヶ月が経ちました



はじめに

React Compilerを2024年7月末に導入し、2024年10月中旬からプロダクション利用を開始して現在(2025年5月中旬)に至ります
これまでに起きたことや感想を共有したいと思います

前提

技術スタック

  • Next.jsのPages Routerを使用し、サーバー側で動く処理はほぼない
  • react-hook-formを使用
  • reactStrictModeはtrue
  • 導入時にeslint-plugin-react-compilerを使った検査では違反なし
  • 各ライブラリのバージョンアップは随時行っている
    // 2024年7月末の導入時
    next: 15.0.0-rc.0
    react: 19.0.0-rc-01172397-20240716
    babel-plugin-react-compiler: 0.0.0-experimental-938cd9a-20240601
    react-hook-form: 7.52.1
    
    // 2024年10月中旬のプロダクション利用開始時
    next: 15.0.0-rc.1
    react: 19.0.0-rc-cd22717c-20241013
    babel-plugin-react-compiler: 0.0.0-experimental-fa06e2c-20241016
    react-hook-form: 7.53.0
    
    // 2025年5月中旬の執筆時
    next: 15.3.2
    react: 19.1.0
    babel-plugin-react-compiler: 19.1.0-rc.1
    react-hook-form: 7.55.0
    

開発スタイル

  • フロントエンドのコードにコミットをする主なメンバーは1~2人
  • 開発者がテストも行う(QAやテスター等開発者以外にテストを行ってくれる人はいない)

対象読者

  • React Compilerの導入を検討している人

結論

  • プロダクションでReact Compiler起因の不具合はまだ1つも起きていない
  • React Compilerは2025年5月中旬時点でRC, Nextではまだexperimentalな機能なので、アップデートのたびに挙動が変わる可能性はある
    • アップデートによりアプリケーションの動作が期待通りに動かなくなったことはない
    • Next.js 15.3.0まではReact Compilerが効いていたが、15.3.1, 15.3.2では恐らく効かなくなった
  • 私の経験ではこれまでに意図した通りに動かなくなった箇所はrefとreact-hook-formを使っている箇所のみ
  • next devnext buildで挙動が変わるケースがよくあった
  • 開発中に、原因がわからず意図した通りに動かなくなることはあるが、一貫して同じ箇所が動かなくなるので気づきやすい
  • useMemo, useCallback, React.memoは今後原則書かないことに決められたのがよかった
  • Reactやreact-hook-formと向き合う時間が取れないチームは採用しない方が良い

本文

React Compilerとは?

Reactアプリを自動的にメモ化するビルドツールです
useMemo、useCallback、React.memoなどを書かなくても、よしなに最適化してくれるイメージです

詳しくは公式ドキュメントを参照してください。ボリュームは少ないので読みやすいと思います。日本語版の公式ドキュメントもありますが、英語版の方がアップデートが進んでいることがあります

プロダクションへの導入きっかけ

リリース前に開発者のみがアプリケーションを利用する期間があった

当時プロダクションのデータ構造を大幅に変更する必要があり、それまで使っていたアプリケーションはそのまま運用し続け、新たにアプリケーションを複製して開発を進めることにしました

開発開始からリリースまでの3ヶ月程は、壊しながら開発をしても問題がないため気軽に変更を反映でき、またリリース前には全ての機能をテストする想定でいたため、当時experimentalだったReact Compilerも導入しやすい状況でした

簡単に剥がせるから

導入するのも剥がすのもNext.jsの設定ファイルを1項目変えるだけでほぼノーコストなので、導入を断念したとしても無駄な時間はあまり発生しない見込みだったのも、大きな理由でした

import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  experimental: {
    reactCompiler: true,
  },
}
 
export default nextConfig

リリースまでに修正したReact Compilerの問題

React Compilerを有効にしてから意図した通りに動かなくなるケースがありました
私の経験では、動かなくなったのは全てreact-hook-formかrefを使用しているコードでした

ref

refを使ったコードが一箇所だけ動かなくなりました。決まった行数を超えると「続きを読む」が出てきて、決まった行数以降を省略するコンポーネントです
決まった行数を超えたかどうかを判定するカスタムフックとコードは元々こんなイメージでした

export const useTruncateTextBlock = () => {
  const [isTruncated, setIsTruncated] = useState(false)

  const measuredRef = (node: HTMLDivElement | null) => {
    if (!node) {
      return
    }
    setIsTruncated(node.scrollHeight > node.offsetHeight)
  }

  return {
    isTruncated,
    measuredRef
  }
}

export const ClippedText: React.FCProps> = ({ children, maxLines = 12 }) => {
  const [isOpen, setIsOpen] = useState(false)
  const { measuredRef, isTruncated } = useTruncateTextBlock()

  return (
    div>
      div ref={measuredRef} css={contentStyle(isOpen, maxLines)}>
        {children}
      div>
      {isTruncated && !isOpen && ReadMoreButton setIsOpen={setIsOpen} />}
    div>
  )
}

原因ははっきりしませんが、親コンポーネントでのタブの切り替えに、display: noneを使っているのが関わってそうな気がしています

今回は高さの算出にResizeObserverを使うことで解消しました

export const useTruncateTextBlock = () => {
  const [isTruncated, setIsTruncated] = useState(false)

  const measuredRef = (node: HTMLDivElement | null) => {
    if (!node) {
      return
    }
    const observer = new ResizeObserver(([entry]) => {
      if (!entry) {
        return
      }
      const target = entry.target as HTMLDivElement
      setIsTruncated(target.scrollHeight > target.offsetHeight)
    })

    observer.observe(node)

    return () => {
      observer.disconnect()
    }
  }

  return {
    isTruncated,
    measuredRef
  }
}

react-hook-formで一貫して動かなかった書き方

色々と意図した通り動かないことがありましたが、まず規則性があったのがwatchformStateです

  • watchが動かない
    • useWatchを使うことで解消した
    • react-hook-formの7.55.0だとwatchでも動いてそう
  • formStateから正しい状態が取れない
    • useFormStatusを使うことで解消した
    • useFormContextの戻り値のformStateが特に動かなかった記憶

react-hook-formで動いたり動かなかったりした書き方

動いている箇所もあれば動かない箇所もあったのがvaluesregisterでした

  • useFormのvaluesが変わっても更新されないことがあった
    • defaultValuesを使い、親コンポーネントのpropsのkeyに、valuesを入れて解消した
    • react-hook-formの7.55.0だとvaluesも動いてそう
  • registerを使っていると動かなくなる箇所があった
    • controlを使うことで解消した
    • registerを使って動いている箇所もたくさんある
    • 同display:noneを使って描画切り替えをしているのが怪しい

next devでは動いていたのに、next buildをすると動かなくなることが何度かあった

React Compiler導入前までは、基本的にローカル開発時にはnext devでの動作確認をするだけで、一応開発環境にデプロイした後(next buildをしている)に動作確認をして挙動が変わることはほぼありませんでした
React Compilerを有効にしてからは、next buildのときだけ動かなくなることが何度かあったので、react-hook-formに関わる変更をした際は、ローカルでもnext buildをして動作確認するようにしました

フォームの数が多くない(useFormの宣言が38箇所)アプリケーションのため、ローカルでnext buildをして動作確認をする機会は少ないですが、フォームが多いアプリケーションだと大変かなと思います

確認と修正を繰り返すときに、next buildの待ち時間が長く感じるようになったので、next devnext buildのアウトプットディレクトリを別にし、next devで動作確認している間にnext buildを実行しておく小技を導入しました

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  reactStrictMode: true,
  distDir:
    process.env.NODE_ENV === 'production' 
      ? '.next'
      : '.next-dev',
  experimental: {
    reactCompiler: true
  }
}

export default nextConfig

Next.js 15.3.1, 15.3.2ではReact Compilerが効いていなさそう

Next.js 15.3.1-canary.9以降、React Compilerは効いていないのではないかと思われます
Next.js 15.3.1-canary.8まではReact Compilerが効いている様子だったため、そう判断しています
その後は修正と切り戻しが何度か繰り返されている様子です

この記事を書くにあたって以前動かなかった書き方を試したところ、全て問題なく動くようになっていたので、しばらくテンションあがっていたのですが、React Developer Toolsを使って確認したところ、Next.js 15.3.0までは自動でメモ化されていたコンポーネントが軒並みメモ化されなくなっていました

React Compiler導入後の変化

不要な再レンダリングが減った

当然ですがReact Compilerが効いている場合は再レンダリングは減っています

ただユーザーの体験としてほぼ何も感じ取れません
前述の通りNext.js 15.3.2ではReact Compilerが効いてなさそうですが、この記事を書くまで気づいていませんでした

useMemo, useCallback, React.memoは原則増やさないことにした

メモ化するかどうかはReactのドキュメントには下記のように書いてあります

https://ja.react.dev/reference/react/useCallback#should-you-add-usecallback-everywhere

https://ja.react.dev/reference/react/useMemo#should-you-add-usememo-everywhere

https://ja.react.dev/reference/react/memo#should-you-add-memo-everywhere

「useCallbackはとにかく使え! 特にカスタムフックでは」の主張に共感しており、メモ化するかどうかの基準に複雑さは持ち込みたくないです

再レンダリングの抑制をすることで、ユーザー体験が向上する箇所はこれまでほぼなかったため(前述の通りメモ化できていないことに気づいていなかったことが答えです)、React Compiler導入後には、新たに書くコードへはuseMemo, useCallback, React.memoを増やさないことにしました

一部React的には好ましくない、メモ化をしないとuseEffectの無限ループが発生してしまう箇所があることもあり、これまで書いていたコードを直すことは積極的には行っていませんが、よりわかりやすい方針にできたのは嬉しいです

ちゃんと動いていた箇所がいつの間にか壊れていたことはまだない

アプリケーションの移行期や、新たにフォームを実装する場合は、React Compilerを有効にした場合のみ動かないケースはありますが、その後のパッケージアップデートや、フォームに軽微な機能追加、修正等を行って、再び壊れたことはまだないです
ここはなんとなくありそうで毎度不安になるのですが、意外とそうでもないのでよかったです

React Compilerの有効化をおすすめ出来ないチームやアプリケーション

Reactの方針に沿ったコードになっていない箇所が多かったり、react-hook-formと向き合う時間が取れない場合は、何かがおかしくなったときに疑う要因がさらに増えるので辞めておくのが良いと思います

react-hook-formはdefaultValuesにundefinedを使うのを避けるべきformStateを購読する場合の論理演算子の使い方に注意、といった、一見するとJavaScriptやTypeScript的には問題ないので、好ましくないコードを書いても気づかないことが多いです

ここにさらにReact Compilerが入ってくると、思った通りに動かない要因がさらに増えるので、かなり混乱すると思います

また、既存のアプリケーションにいきなり全適用するのは結構怖いと思います
やるなら全てテストする気持ちで臨むか、Next.jsのannotationモードを使って段階的に導入するのが良いと思います

まとめ

たまたま絶好の機会があったおかげでReact Compilerを試すことができ幸運でした
"use no memo"で部分的に逃げるか、"use no memo"だらけになりそうなら大人しく無効にすればよいというのも非常に気楽でした

また、壊れる理由がわからないことはあっても、壊れるときは一貫した挙動をしてくれる、いつの間にか壊れることも今のところないのも嬉しいポイントです

私のアプリケーションの場合はメモ化によるユーザー体験向上はほぼないと思いますが、開発者としてメモ化に関するコードは書かないと決められたことが大きな収穫でした

各種ライブラリのアップデートでどんどん使いやすくなっている様子(今は一時的に効いていないようですが)なので、これからアプリケーションを作り始める人は導入を検討してみると良いと思います

フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link

Views: 1

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -

インモビ転職