概要
Next.jsはReactをベースとしたフロントエンドフレームワークである。開発体験と本番環境におけるパフォーマンスを両立させるための仕組みを提供することにある。これを実現するため、用途に応じて2つの主要な動作モードが用意されている。
開発サーバー (Development Mode)
ローカル環境での開発時に用いるモードであり、npm run dev コマンドで起動する。開発効率の最大化のため、プロダクションより遅い。
- ホットリロード機能: コードの変更を検知し、ブラウザを自動的にリロードする。これにより、修正結果を即座に確認できる。
- オンデマンドコンパイル: ページへのアクセスが発生するたびに、その場でTypeScriptからJavaScriptへのコンパイルが実行される。ターミナルに表示される
compile: 86msといったログがこれに該当する。 - トレードオフ: 頻繁なコード変更に追従できる利便性を持つ一方、リクエストごとにコンパイルが挟まるため、応答速度は本番環境に比べて著しく遅くなる。これは開発効率を優先した結果の、意図された仕様である。
プロダクションビルド (Production Mode)
Webサイトを一般公開する本番環境で用いるモード。npm run build でコードをビルドし、npm run start でサーバーを起動するという2段階のプロセスを踏む。
- 事前コンパイル:
npm run buildコマンドを実行することで、アプリケーション全体のコードが事前に一括でコンパイル・最適化される。 - 高速な配信:
npm run startコマンドは、ビルド済みの静的ファイルを配信する役割に特化している。リクエストごとにコンパイルが走ることはないため、極めて高速に応答を返すことが可能となる。 - トレードオフ: ユーザーへの応答性能を最大化する設計であるため、ホットリロードのような機能は存在しない。コードの変更を反映させるには、再度
npm run buildを実行する必要がある。
動作モードの比較
要するに Development Mode は遅いのでプロファイリングするならプロダクションビルドでやらないといけない。
| 項目 | 開発サーバー | プロダクションビルド |
|---|---|---|
| 起動コマンド | npm run dev | npm run build → npm run start |
| ページ表示速度 | 遅い(150〜260ms程度) | 速い(数ms〜数十ms) |
| コンパイル | アクセス毎に実行 | 事前に一括で実行 |
| コード変更の反映 | 即時(ホットリロード) | 再ビルドが必要 |
| 主な用途 | ローカルでの開発 | 本番環境での運用 |
開発のための知識
.next/dev/lock
同じディレクトリで複数の起動はできない。
3000 と 3002 の同時起動について 同じ .next/ ディレクトリを共有するため、同時起動はできません。 ポートが別でも .next/dev/lock は1つなので、後から起動する方が Unable to acquire lock エラーになります。
ポート3000 (人間用) ─┐
├→ 両方とも frontend/.next/ を使う → 競合
ポート3002 (テスト用) ─┘
自動テストの前に、人間用もテスト用も落としておかないといけない。
賢くやるより、fuser で強制的に落としたほうが楽
fuser -k 3000/tcp 2>/dev/null; sleep 2; ss -tlnp | grep 3000 || echo "ポート3000のLISTENプロセスなし"
fuser -k 3002/tcp 2>/dev/null; sleep 2; ss -tlnp | grep 3002 || echo "ポート3002のLISTENプロセスなし"
Next.jsにおけるナビゲーションの仕組み
Next.jsでは、ユーザー体験とパフォーマンスのバランスを取るために、2種類のナビゲーション手法を使い分けます。
Loading diagram...
1. ハードナビゲーション (window.location.href / ページ更新)
ブラウザの標準的な遷移機能を利用する仕組みです。
- 動作フロー:
- リクエスト: ブラウザがサーバーへ対象URLのデータを要求します。
- 生成: サーバーが最新のHTMLを生成して返します。
- 描画: ブラウザが現在の状態をすべて破棄し、受け取ったHTMLで画面全体を描き直します。
- 特徴: アドレスバーにURLを入力してEnterを押す動作と同等です。画面が一瞬白くなる(マウント解除される)ため体感速度は落ちますが、サーバー上の最新の状態が確実に反映されます。
2. ソフトナビゲーション (router.push() / Linkコンポーネント)
Next.js(JavaScript)がブラウザの履歴API(History API)を制御する仕組みです。
- 動作フロー:
- リクエスト: JavaScriptが遷移先に必要なデータ(RSC Payload)のみをサーバーに要求します。
- 差分更新: 画面全体をリロードせず、変更が必要なコンポーネントのみを書き換えます。
- 特徴: 画面のちらつきがなく、非常に高速で滑らかな遷移が可能です。ただし、**ブラウザ側に保持されるキャッシュ(Router Cache)**の影響を受けやすくなります。
キャッシュによる「不整合」のメカニズム
ご提示いただいた「保存したのに内容が変わらない」という現象は、サーバー側のキャッシュとクライアント側(ブラウザ)のキャッシュの役割の違いから生じます。
サーバー側とクライアント側のキャッシュ構造
| キャッシュの種類 | 管理場所 | 役割 | 制御方法 |
|---|---|---|---|
| Full Route Cache | サーバー | revalidate = 60 等の設定に基づき、HTML/RSCデータを保持する | revalidatePath で削除可能 |
| Router Cache | ブラウザ | 一度訪れたページのデータを一時的にメモリに保存する | router.refresh() や Server Actions で制御 |
API(Route Handler)とServer Actions
| 項目 | API (Route Handler) | Server Actions |
|---|---|---|
| 呼び出し方 | fetch("/api/...") | 関数の実行 await action() |
| 主な戻り値 | JSON, XML, etc. | 成功/失敗 または 最新のRSC Payload |
| キャッシュ同期 | 手動(router.refresh() などが必要) | 自動(revalidatePath 等と強力に連動) |
| 主な用途 | 外部システムとの連携 | 自アプリ内のフォーム送信・データ更新 |
| セキュリティ | CORS設定などが必要な場合がある | Next.jsによりCSRF保護等が自動適用される |
API (Route Handler)
標準的なHTTPプロトコルに基づいた設計です。フロントエンド(Client)とバックエンド(Server)が完全に分離した状態で通信します。
- 通信方式: クライアントが
fetch()を使用して、特定のURL(例:/api/save)にリクエストを送ります。 - 状態管理: サーバーはデータ(JSONなど)を返却するだけで、クライアント側の画面状態やキャッシュには関与しません。
- 用途: 外部サービスからのWebHook受信や、モバイルアプリなどNext.js以外からも利用されるエンドポイントの作成に適しています。
Loading diagram...
Server Actions
Next.jsのフレームワークに深く統合された、サーバー実行関数です。クライアントから「関数」として直接呼び出しますが、実行はサーバーで行われます。
- 通信方式:
use serverと定義された関数を呼び出すと、Next.jsが自動的にPOSTリクエストを生成します。 - 状態管理: サーバー側で
revalidatePathなどを実行すると、そのレスポンスの中に「新しい画面データ(RSC Payload)」が含まれます。 - 連動性: データの更新と「ブラウザが持つキャッシュの破棄」「画面の再描画」が、1回のリクエスト/レスポンスの中で完結します。
Loading diagram...
Route Handler と Server Actions でキャッシュの処理が全く違う
どちらも同じ関数(next/cache からインポートするもの)を使いますが、「どこで呼ぶか」によって、ブラウザ側のキャッシュ(Router Cache)に対する影響力が全く変わります。
1. revalidatePath の呼び出し場所による比較
| 項目 | Route Handlers(API経由) | Server Actions 内部 |
|---|---|---|
| 主な呼び出し方 | fetch('/api/save') | <form action={saveAction}> または await saveAction() |
| サーバー側キャッシュ | 即座に無効化される | 即座に無効化される |
| ブラウザ側キャッシュ | 無効化されない(手元のメモリに残る) | 即座に無効化される(最新化される) |
| 再生成のタイミング | 次のアクセス時(SWR挙動) | そのリクエスト内で最新化 |
| Next.jsの動作 | 単なるHTTP通信として処理 | データ更新 + 画面同期の特殊通信 |
A. Route Handler(API)で呼ぶ場合
APIを叩いた時、Next.jsは「データの変更」は認識しますが、それをブラウザに伝える手段がありません。
Loading diagram...
B. Server Actions で呼ぶ場合
Server Actions の場合、Next.jsが通信を乗っ取り、レスポンスの中に「キャッシュ破棄命令」と「最新の画面データ」を詰め込みます。
Loading diagram...
Next.js の内部実装では、Server Action のレスポンスヘッダーに特殊な指示(x-action-revalidated など)が含まれます。
- API(Route Handler): ブラウザは
fetchの結果を単なるデータ(JSON等)として受け取ります。ブラウザ側の Next.js ランタイムは、その背後でrevalidatePathが呼ばれたことを検知できません。 - Server Actions: Next.js が提供する
actionという仕組みを通じて通信するため、サーバー側でrevalidatePathが呼ばれると、ブラウザ側の Next.js ランタイムへ「今すぐ/test/FrontPageのキャッシュを捨てて、同封した最新データに差し替えろ」とダイレクトに命令が下ります。
問題が発生するフロー
- Next.jsの revalidatePath は、サーバー上のキャッシュを即座に最新へ書き換えるのではなく、「現在のキャッシュを期限切れ(Stale)としてマークする」だけです。そのため、変更直後の最初のリクエストには古いデータが返されます。
Loading diagram...
-
revalidatePathはサーバー側のキャッシュを無効化しますが、**「すでにブラウザのメモリに乗っているキャッシュ(Router Cache)」**を強制的に書き換える力は、API経由の呼び出しでは限定的 -
Stale-While-Revalidate (SWR)
- SWRは、**「とりあえず手元にある古いデータ(Stale)を出しつつ、裏側で最新版に更新(Revalidate)する」**というキャッシュ戦略。サーバーはキャッシュが期限切れだと知っていても、手元にある「古いHTML」を即座に返します。これでユーザーの待ち時間はゼロになります。
-
Next.jsのISR(Incremental Static Regeneration)には、実は2段階の有効期限があります。
- Revalidate期間 (revalidate = 60): 60秒経ったら「そろそろ更新の準備をしてね」という合図。
- Stale Time (300秒): サーバーが「どれだけ古くても、この時間内ならキャッシュを使い回していいよ」と判断する強制期間。
-
今回起きたことのタイムライン
- 01:09:01: revalidatePath を実行。サーバー内の test/FrontPage に「期限切れ」のフラグが立ちます。
- 直後のアクセス: 本来ならここで再生成が走るはずですが、Next.js 15/16の next start(本番モード)では、「すでに配信中の古いHTMLをどれだけ維持するか」の最適化が働きます。
- 01:14:31: stale-time の300秒(5分)が経過したタイミングで、サーバーがようやく「もうこれ以上古いのは出せない」と判断し、バックグラウンドでのHTML生成プロセスを完了させました。