Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TSKaigi 2026 アンチパターンを避ける型駆動React最適化

Avatar for seriseri seriseri
May 22, 2026
280

TSKaigi 2026 アンチパターンを避ける型駆動React最適化

React 19の登場とともに広く普及したReact Compilerは、コンポーネント最適化のあり方を大きく変えました。これまでuseMemoやuseCallback、React.memoに依存していた手動メモ化は、多くのケースでコンパイラによって自動化されつつあります。

ただし、副作用やミュータブルな操作を含むコードでは最適化は適用されず、その仕組みの理解は依然として重要です。

本セッションでは、React CompilerがASTとデータフロー解析によって依存関係と副作用を検出し、メモ化境界を決定する仕組みを解説します。また、最適化を阻害するアンチパターンを整理し、TypeScriptの型システムや Biome / Oxc といったツールを用いた検知・防止方法を紹介します。

さらに、手動メモ化の負担軽減に伴い、設計の重心が「再レンダリング最適化」から「純粋性・副作用分離・責務の明確化」へ移行している点を踏まえ、大規模プロジェクトにおける最新の設計指針も提案します。

Avatar for seriseri

seriseri

May 22, 2026

Transcript

  1. SECTION 00 本日お話しすること Agenda 01 イントロ なぜ今 React Compiler か

    / 手動メモ化の限界 02 コンパイラの仕組み AST + データフロー解析 / メモ化境界の決定 03 最適化を阻害するアンチパターン 副作用・ミュータブル・参照不安定 04 型駆動で防ぐ TypeScript で純粋性を表現する 05 Biome / Oxc で機械的に守る Lint と CI でのガードレール 06 大規模プロジェクトの新しい設計指針 再レンダ最適化から責務分離へ 02 / 24 PeopleX
  2. SECTION 01 手動メモ化のコスト なぜ「手動メモ化地獄」が生まれたのか TSX // “とりあえず全部メモ化 ”の典型 const Row

    = memo((props) => { const label = useMemo(() => formatLabel(props.user), [props.user]); const onClick = useCallback(() => { props.onSelect(props.user.id); }, [props.user.id, props.onSelect]); return <li onClick={onClick}>{label}</li>; }); 実装現場で起きていたこと 依存配列の管理コスト exhaustive-deps と毎回戦う / lint 抑制が常態化 メモ化漏れ・誤メモ化 新メンバーは正しく書けない / 直しても効かない レビューの大半が再レンダ議論 本質的なロジック議論にたどり着かない バンドルサイズと可読性の悪化 プロダクトコードがメモ化で埋め尽くされる 04 / 24 PeopleX
  3. SECTION 01 React Compiler の登場 React 19 + React Compiler

    が前提を変えた ✕ これまで 手動メモ化 useMemo useCallback React.memo → ◦ これから コンパイル時の自動メモ化 AST + データフロー解析で自動挿入 → ◦ そのために必要 純粋なコンポーネント 純粋なフック 副作用なし / ミュータブル操作なし React Compiler が行うこと ▪ コンポーネントとフックを AST に変換し、参照グラフ・依存関係を静的に解析 ▪ 純粋(同じ入力 → 同じ出力 / 副作用なし)な範囲を自動でメモ化境界として切り出し ▪ ランタイムでは React Forget スタイルのキャッシュにより props / state ごとに再計算を回避 ▪ 副作用 / ミュータブル操作を含むコードは“最適化されない”だけで、壊れはしない 05 / 24 PeopleX
  4. SECTION 01 設計の重心が動く 「再レンダ最適化」から「純粋性・副作用分離」へ BEFORE React 18 まで useMemo /

    useCallback / memo を“手で”張る 依存配列の正確さがチームに依存 再レンダリングが設計の中心トピック 純粋性は“あった方が良い”という温度感 AFTER React 19 + Compiler メモ化は基本コンパイラに任せる 純粋性・参照安定性が“仕様”になる 設計中心は責務分離と副作用の隔離 型 / Lint が最適化前提を守るゲート 06 / 24 PeopleX
  5. SECTION 02 コンパイラパイプライン 主要な5フェーズに整理した React Compiler の処理 1 Parse JSX

    / TS → AST ソースから抽象構文木へ › 2 HIR 中間表現の構築 制御フローと参照を抽出 › 3 Dataflow データフロー解析 値の依存・到達定義を追跡 › 4 Effect Infer 副作用 / 可変性の推論 純粋性を満たす範囲を特定 › 5 Memoize メモ化境界の挿入 キャッシュキー付きで再書き出 し ポイント:副作用や可変性が検出された箇所は、コンパイラが安全側に倒して最適化を“諦める” → つまり、最適化されるか否かは “純粋性が証明できるか ”で決まる 08 / 24 PeopleX
  6. SECTION 02 データフロー解析 コンパイラは “何が変わると何が変わるか ”を見ている TSX function UserCard({ user,

    theme }) { const name = user.firstName + ' ' + user.lastName; const color = theme.primary; const style = { color }; return ( <Card style={style}>{name}</Card> ); } 解析結果(概念) name ← user.firstName, user.lastName color ← theme.primary style ← color JSX ← name, style → user / theme どちらかが参照等価で変わらなければ JSX 全体をキャッシュする 09 / 24 PeopleX
  7. SECTION 02 最適化される / されない “純粋に書けているか ”が判定基準 ◯ 最適化される TSX

    function Total({ items }) { const total = items.reduce( (a, b) => a + b.price, 0 ); return <p>{total}</p>; } 入力 items を読み取って単一値を返すのみ。外部状態への副作用なし。 ✕ 最適化されない TSX function Total({ items }) { let total = 0; items.forEach(i => { total += i.price; // local accumulation }); logger.info(total); // side effect return <p>{total}</p>; } render 中に logger.info() で外部世界へ。副作用が “諦め”の根本要因。 10 / 24 PeopleX
  8. SECTION 03 全体像 コンパイラに “諦められる ”4つのパターン A 副作用の混入 render 中の

    I/O・ログ・乱数 ・Date.now() B ミュータブル操作 let の再代入 / push / sort / 直接代 入 C 参照不安定 毎回新規生成のオブジェクト / 関 数を子に渡す D 非決定的な依存 外部スコープのクロージャ越し可 変参照 12 / 24 PeopleX
  9. SECTION 03 A: 副作用 / B: ミュータブル操作 render 中の I/O

    と let / push を見直す A. 副作用の混入 NG function Now() { const t = Date.now(); // NG return <p>{t}</p>; } OK function Now() { const t = useNow(); // OK return <p>{t}</p>; } B. ミュータブル操作 NG const sorted = items; sorted.sort((a, b) => a.id - b.id); // props.items を破壊 OK const sorted = [...items].sort( (a, b) => a.id - b.id ); 「render は値を返す関数」 — 副作用は useEffect / イベントハンドラ / 専用フックに切り出す 13 / 24 PeopleX
  10. SECTION 03 C: 参照不安定 / D: 非決定的な依存 props 境界と外部参照の取り扱い C.

    参照不安定(境界を超える毎回 new) NG <Child options={{ size: 'lg' }} /> // 親が再レンダする度に新規オブジェクト OK const OPTIONS = { size: 'lg' } as const; <Child options={OPTIONS} /> // or: 親側で純粋に算出 → 自動メモ化が効く D. 非決定的な依存(外部の可変参照) NG let counter = 0; // module scope function Badge() { counter++; // NG: 外部可変参照 return <span>{counter}</…>; } OK function Badge() { const count = useCounter(); return <span>{count}</…>; } 「render は入力(props / state / context)だけに依存」 — モジュールスコープの可変参照は禁じ手 14 / 24 PeopleX
  11. SECTION 04 型による不変性の表明 Readonly でミュータブル操作を “書けなく”する TS // props を

    deep readonly で受ける type Props = { items: ReadonlyArray<Item>; user: Readonly<User>; }; function List({ items }: Props) { items.push(...) // ✕ コンパイルエラー const next = [...items, x]; // ◦ } 型で禁止すれば、 Compiler の検出を待たない IDE で即座にフィードバック 書いた瞬間に赤線が出る .push / .sort などの破壊的 API を遮断 Array.prototype の禁則をビルド前に止める DeepReadonly ヘルパで構造全体に伝播 props / state ツリー全体を不変化 “何をしてはいけないか ”がコードで読める ドキュメントを別に持たなくて済む 16 / 24 PeopleX
  12. SECTION 04 純粋関数を型で表現する Branded type と Pure<T> で“純粋契約”を型化 TS //

    純粋関数を branded type で表現 declare const PURE: unique symbol; type Pure<F> = F & { readonly [PURE]: true }; function asPure<F extends (...a: any[]) => any>(fn: F): Pure<F> { return fn as Pure<F>; } // コールバックは Pure を要求 → 副作用関数は渡せない type TableProps<T> = { rows: ReadonlyArray<T>; rowKey: Pure<(row: T) => string>; }; → rowKey に副作用付き関数を渡すと型エラー。Compiler の前段で“純粋契約”を強制できる。 17 / 24 PeopleX
  13. SECTION 05 Lint で前提を守る Biome と Oxc で React Compiler

    の前提を強制 Biome Rust 製の高速 Lint / Formatter noParameterAssign — 引数の再代入を禁止 eslint-plugin-react-hooks 併用で純粋性ルールをカバー noUselessFragments / noUselessRename — render を清潔に保つ react-hooks/purity, immutability, globals で副作用・可変参照を検出 Oxc Rust 製のオールインワン JS/TS ツールチェイン oxlint react/exhaustive-deps — 依存配列の網羅性 oxlint react-hooks/* — フック規約を高速検査 Compiler 専用ルールは ESLint プラグインで補完するのが現実的 巨大コードベースでも秒オーダーで完走 → pre-commit / CI 向き 19 / 24 PeopleX
  14. SECTION 05 多層防御モデル “純粋性が壊れない仕組み ”を4層で組む L1 型システム TypeScript / Readonly

    / Pure<T> コンパイル前に弾く L2 Linter eslint-plugin-react-hooks / Biome / Oxc 保存 / commit 前に弾く L3 React Compiler AST + データフロー解析 ビルド時に判定し最適化 L4 ランタイム React DevTools / Profiler 観測でリグレッション発見 20 / 24 PeopleX
  15. SECTION 06 コンパイラ時代の責務分離 純粋層 / 副作用層 / UI 層を物理的に分ける Effects

    I/O / fetch / storage / log 副作用は専用フック・サービスに隔離 Domain 純粋関数 / Pure<T> / 不変データ Compiler が最適化できる純粋層 UI JSX / Server Components 純粋層の値を “ただ描画する ”だけ チームに残すルール 1 副作用は“フック / サービス”に追い出す render 中に I/O を呼ばない 2 ドメイン関数は Pure<T> を強制する TS 型で副作用を弾く 3 UI は純粋層の値を渡すだけ props は ReadonlyArray / Readonly 4 メモ化は書かない、書かせない lint で useMemo/useCallback を制限 22 / 24 PeopleX
  16. SECTION — まとめ Key Takeaways 01 メモ化はコンパイラの仕事へ useMemo / useCallback

    / memo を“貼る”のは過去の作業 02 判定軸は「純粋性が証明できるか」 副作用 / 可変性 / 参照不安定があると最適化は諦められる 03 型と Lint で前提を守る Readonly / Pure<T> / Biome / Oxc で書けなくする 04 設計の重心は責務分離へ Effects / Domain / UI を物理的に分け、純粋層を厚くする 最適化されるコードは、結果として “読みやすく、テストしやすく、壊れにくい ”コードである。 23 / 24 PeopleX