🚀

クライアントコンポーネント主体で進める Rails から Next.js App Router 移行の現実解

に公開

クライアントコンポーネント主体で進める Rails から Next.js App Router 移行の現実解

はじめに

こんにちは、READYFOR のテックリード兼フロントエンドエンジニアの菅原(@kotarella1110)です!

昨年の Advent Calendar では、Next.js App Router アプリケーションの実運用化に向けた PoC を実施し、セルフホスト環境での Next.js のキャッシュについて紹介しました。

https://zenn.dev/readyfor_blog/articles/5980c19ac10621

READYFOR はクラウドファンディングのプラットフォームを運営しており、フロントエンドは Rails アプリケーションに React on Rails を組み込んで React コンポーネントをレンダリングする構成です。
数年前のフロントエンド分離戦略により、SSR が不要な領域は Next.js Pages Router による SPA へ移行しました。一方、SSR が必要な領域では React on Rails が使用され続けていました。今回、この未移行の領域についても Next.js App Router で SPA 化することを決定しました。
ただし、READYFOR では Node.js サーバーの実運用経験がなく、技術的な不確実性が高い状況でした。そこで昨年、上記記事で紹介した通り PoC を実施しました。

PoC から1年経過した現在、一部のページの Next.js App Router 化が完了し本番稼働しています。本記事では、なぜ SPA 化を決断したのか、どのような移行戦略を取ったのか、そして実際の移行はどうだったのかを紹介します。

SPA 化を決断した理由

SSR が必要な領域を SPA 化することを決定した背景には、主に3つの理由があります。

理由1: React on Rails の課題を解消できる

READYFOR での React on Rails 構成には、パフォーマンスや開発効率に関する課題がありました。これらの課題は React on Rails Pro や Shakapacker の導入、使い方の工夫で改善可能なケースもあるため、本節では React on Rails を批判する意図はないということを最初に明示しておきます。

以下に課題について記載しますが、Next.js による移行だけで特別な対応せずとも解決されます。

巨大な JavaScript バンドルによるパフォーマンス劣化の解消

全ページの JavaScript が1つの巨大なバンドルにまとめられており、どのページでも全ページ分のコードが読み込まれてしまっている状況です。あるページでは約 690KB の JavaScript が読み込まれ、そのうち約 380KB(55%)は使われていないコードでした。そこで試しに未使用コードを削除して計測してみたところ、FCP / LCP / TBT の指標が改善し、Lighthouse のパフォーマンススコアが平均約20点向上しました。

Next.js ではページごとに JavaScript バンドルが自動分割され、必要最小限のコードのみが読み込まれるため、このパフォーマンス問題が解消されます。

ハイドレーションのタイミング問題の解消

SSR で送信された HTML をインタラクティブにするには、ハイドレーションが必要です。通常、React のハイドレーションは DOM 構築完了時(DOMContentLoaded)に実行されるべきですが、当時の React on Rails v14 では全リソース読み込み完了時(load)に実行されていました。[1]

ハイドレーション開始が遅れることで、パフォーマンススコア(TTI)の悪化や、useEffect で記述したクライアント処理の実行遅延という問題が発生していました。READYFOR では独自パッチを当てて DOMContentLoaded で実行するよう対応していました。

ExecJS の互換性制約からの解放

React on Rails は SSR を ExecJS で実行しています。ExecJS は Ruby から JavaScript を実行するライブラリですが、Node.js API や最新の JavaScript 機能には対応していません。そのため、Node.js API や最新機能に依存するライブラリを使用する際、Polyfill の追加や、ExecJS で動作するファイルが読み込まれるようビルド設定を調整する必要がありました。特定のライブラリをアップデートすると互換性問題が発生し、調整に大きなコストがかかることがありました。

理由2: より良い UX の提供が可能に

当時、Rails で構築されたページに対しても、以下のような UX を実現したいという要望がありました。

  • 各フィルタやソートの状態を URL に反映し、リンクの共有やブックマークが可能
  • ページ遷移やフィルタ・ソート時に画面全体をリロードせず、コンテンツ一覧のみを更新

これらの要望に対応するにはルーティングでの SSR と CSR のシームレスな統合が必要です。
このような UX を Rails で実現するとなると、React on Rails には SSR と CSR をシームレスに統合する機能がないため、フロント側ではクライアントサイドルーティングや、サーバー・クライアント間のデータ引き継ぎを独自に実装する必要があります。また、バックエンド側でも CSR 用の API エンドポイントを用意する必要があり、それなりの実装・保守コストが発生します。更に、これらを実現するのをライブラリを使用すると、理由1で述べた JavaScript バンドルの問題をより深刻化させることになります。

一方 Next.js では、ページリクエスト時は SSR 、ページ遷移時には CSR として動作する仕組みを標準で提供しており、ルーティングやデータ取得の統合もシームレスに行うことが可能です。

あくまでこれは一例ですが、Next.js による SPA 化を進めることでより良い UX の提供に集中できるようになります。

理由3: エンジニアリングの成長戦略のための基盤整備

READYFOR では Rails モノリシックアーキテクチャからの脱却を進めており、将来的に複数の新規プロダクトの立ち上げが計画されています。これらの中には SSR 構成を満たす必要があるものも含まれる可能性が高いです。

しかし、組織として Node.js サーバーの実運用経験がなくノウハウが蓄積されていない状態で新規プロダクトの開発に着手すると基盤構築を一から始める必要があり、開発開始までのリードタイムを大幅に延長させるだけでなく開発中の不確実性も高めます。React on Rails による SSR 構成を新規プロダクトで採用することは、READYFOR のアーキテクチャの方針に逆行するため現実的ではありません。

一方で、既存の主力プロダクトで Next.js による SPA を実運用し、そのノウハウを組織に蓄積できれば、実績のある技術スタックを適用することで新規プロダクトの立ち上げリードタイムの短縮に期待できます。また、実運用で培ったベストプラクティスやアンチパターンの知見を活かすことで開発時の試行錯誤を最小化できます。

エンジニアリングの成長戦略を実現する上で、Next.js による SPA の実運用ノウハウを今のうちに蓄積することは、将来への重要な投資だと判断しました。

移行戦略

SPA 化を進めるにあたって開発環境の構築やインフラ環境の整備、そしてスムーズな移行を実現するために、開発やテスト方針を定めました。

開発方針

クライアントコンポーネント主体の開発を決断

Next.js App Router はサーバーコンポーネントを最大限活用する開発が一般的ですが、READYFOR では異なるアプローチを選択しています。

READYFOR では全社的なスタイリングライブラリとして Emotion を採用しており、社内の UI コンポーネントライブラリも Emotion で構築されています。Emotion はランタイム CSS-in-JS のためサーバーコンポーネントではサポートされていません。

この制約に対してゼロランタイム CSS への移行も検討しましたが、スタイリングライブラリの移行と App Router への移行を同時に進めると、リスクとコストが倍増します。そこで、まずは「App Router に乗せること」に集中し、スタイリングライブラリの移行は App Router への移行後に段階的に進める方針としました。

そのため、Next.js App Router を採用しつつも、ほぼ全てのコンポーネントをクライアントコンポーネントにするという開発方針を取っています。これは従来通りの SSR アプリケーションを App Router で構築するアプローチです。

この方針では、以下のようにサーバーコンポーネントは主に「データ取得用のラッパー」として利用されます。

サーバーコンポーネント
export async function ProjectDetail({ id }: { id: string }) {
  const project = await fetch(`https://example.com/api/projects/${id}`).then(
    (res) => res.json(),
  );
  return <ProjectDetailPresentation project={project} />;
}
クライアントコンポーネント
"use client";

import styled from "@emotion/styled";

type Project = {
  title: string;
};

export function ProjectDetailPresentation({ project }: { project: Project }) {
  return <Title>{project.title}</Title>;
}

const Title = styled.h1`
  font-size: 24px;
`;

サーバーコンポーネント主体の開発と比べ、バンドルサイズ削減やハイドレーションコスト削減の恩恵が限定的だったり、RSC ペイロードが HTML に含まれるため HTML サイズが増加するといったデメリットはあります。しかし、既存の React コンポーネントをそのまま移植可能で、移行や学習コストが低いことに加え、app ディレクトリの柔軟なレイアウト設計やストリーミングをはじめとした App Router の多くの利点は享受できます。

この判断により、理想的な App Router の使い方ではないものの、現実的な移行戦略を実現できています。

その他の開発方針

  • これまで通り、認証およびセッション管理は Rails が一元的に担い、Next.js はブラウザから送信されたセッション Cookie をそのまま Rails API に転送し、認証状態の判定は Rails のレスポンスに基づいて行う
  • 移植元の React コンポーネントはスタイリングやロジックの変更無しに可能な限りそのまま流用する
    • ただし、データフェッチはなるべく末端のコンポーネントで行い、適切にコンポーネント分割する
    • また、クライアントコンポーネント配下にサーバーコンポーネントを配置したい場合はコンポジションする
  • 移行の段階では Next.js のキャッシュは使用しない(パフォーマンス改善のタイミングで使用する)
  • サーバーコンポーネントを Container、クライアントコンポーネントを Presentational として分離し、クライアントコンポーネントのテスト容易性を向上させる

他にも詳細な開発方針はありますが、主要なものは上記の通りです。
とにかく、移行を最優先に考えた方針であることがポイントです。

テスト方針

サーバーコンポーネントを含むページ全体のテストはブラウザテストを主軸に

基本方針として、サーバーコンポーネントを含むページのテストは Playwright を用いたブラウザテストを主軸に行っています。
Next.js の Experimental test mode for Playwright を有効にしており、これにより MSW を使ってサーバーコンポーネントからの API 通信をモックしつつ、ブラウザ上での振る舞いを確認することが可能です。

ブラウザテストを主軸にする最大の理由は、Next.js App Router アプリケーションの統合的な振る舞いをテストする上で最適だからです。ナビゲーション、エラーハンドリング、ストリーミングといった Next.js の機能は、実際のブラウザ環境でこそ本来の動作を確認できます。Testing Library を用いた jsdom ベースのテストでは、サーバーコンポーネントをサポートしていないだけでなく、これらの統合的な振る舞いを適切にテストすることが困難です。また、実際にユーザーが動かす環境(jsdom ではなく実 DOM を持つブラウザ環境)でのテストは、デバッグも容易 & 信頼性の高いテストを実現できます。[2]

また、クライアントコンポーネントについては従来通り、Storybook で定義した各ストーリーを Testing Library でテストしています。特にユーザーの操作や表示内容の分岐など、細かい挙動の確認をしており、非 UI 部分(純粋な関数、フック、ヘルパーなど)については、Vitest 及び Testing Library を使用し、これまでと同様にユニットテストを実施しています。

実際の移行について

私は、Next.js App Router の社内勉強会の開催、開発環境の構築、開発・テスト方針の策定、SPA 化のための全タスク洗い出しといった準備作業を担当しました。インフラ環境の整備については SRE メンバーと協力して進めました。これらの準備が整った後、実際の移行作業は別のチームが担当し、私は主にレビューや技術的なサポートを行いました。

リリース後にメンバーへヒアリングしたところ、Next.js 特有の問題で大きくハマることはなく、移行作業は順調に進んだとのことでした。パフォーマンスについても期待通りの改善が見られ、Lighthouse のパフォーマンススコアがモバイルでは平均約10点、デスクトップでは約20点向上しました。[3]

移行が順調に進んだ主な要因として、以下の点が挙げられます。

  • 移植元のページが React on Rails により React コンポーネントで記述されていたため、コンポーネント分割、nuqs を活用したルーティング、サーバーコンポーネントによる API 呼び出しへの変更程度で済んだ
  • クライアントコンポーネント主体の開発方針により、Next.js App Router で習得すべき概念が限定され、学習コストが低かった
  • 移行対象がほぼリードオンリーなページだったため、複雑なフォーム処理や状態管理の考慮が不要だった
  • 開発・テスト方針を事前に言語化していたことで、LLM を活用したコード生成がスムーズに行えた

おわりに

本記事では、READYFOR での Rails から Next.js App Router への移行を進めた理由とその戦略について紹介しました。

移行を成功させるポイントは、理想を追い求め過ぎずに現状に合わせた現実的なアプローチを取ることだと感じています。クライアントコンポーネント主体の開発方針は Next.js App Router の理想的な使い方ではありませんが、既存のコードベースを活かしながらスムーズに移行するための実用的な選択でした。

今回の移行で開発・テスト環境や移行プロセスの基盤が整ったので、他のページも順次移行していく予定です。その後は、Next.js のキャッシュ活用によるパフォーマンス改善や、スタイリングライブラリの移行といった次のステップに進んでいきます。実運用で得られた知見は、今後の新規プロダクト開発にも活かしていけると考えています。


明日の READYFOR Advent Calendar 2025 の21日目は @mosmos21 さんによる記事です。お楽しみに!

脚注
  1. v12.0.3〜v13.3.4 までは DOMContentLoaded で実行されていましたが、それ以外のバージョンでは load で実行されていました。 ↩︎

  2. READYFOR では過去に Cypress や Playwright を中心としたブラウザテスト主体のテスト戦略を採用していました。その後、テスト実行速度や Flaky テストの増加といった課題から、Testing Library を用いたインテグレーションテスト中心の戦略へ移行されています。一方で、jsdom 環境におけるデバッグの難しさや、多数のブラウザ API をモックした結果あまり価値の高くないテストが生まれてしまう、といった課題も経験しました。こうした背景から、現在は 「ブラウザでしか検証できない振る舞い」はブラウザテストで担保する、という方針を重視しています。 ↩︎

  3. 移行した5ページ、各ページ3回計測の平均値です。 ↩︎

GitHubで編集を提案
READYFORテックブログ

Discussion