フロントエンド

Next.js 16.3 Instant Navigations入門——Firestore画面遷移を速く見せる

Next.js 16.3 Previewで公開されたInstant Navigationsを、Next.js App RouterとFirestoreの業務画面にどう適用するかを実践形式で解説します。

2026年6月26日
Next.jsApp RouterFirestoreフロントエンド
Next.js 16.3 Instant Navigations入門——Firestore画面遷移を速く見せる

はじめに

2026年6月25日、Next.js公式ブログで Next.js 16.3 Preview: Instant Navigations が発表されました。これは、Server Components中心の設計を保ったまま、SPAのように「クリック直後に次画面の骨格が出る」体験へ近づけるための機能群です。

業務システムでは、Firestoreから詳細データを読む画面で遷移が重く見えがちです。たとえば架空の受発注管理SaaS「OrderBoard」で、注文一覧から詳細へ移動するたびに、顧客情報、注文明細、監査ログ、在庫確認をすべて待ってから表示していると、ユーザーは「画面が止まった」と感じます。

本記事では、公式ブログとプレビューDocsで確認できる範囲に絞り、Next.js App Router + Firestore + TypeScriptの画面でInstant Navigationsを試す手順を解説します。なお、16.3はPreviewであり、公式にも本番ユーザーへの適用は慎重に判断するよう案内されています。

Instant Navigationsで何が変わるのか

公式Docsでは、navigationがinstantである状態を「クリックした瞬間に、静的コンテンツ、キャッシュ済みコンテンツ、fallback UIを表示でき、残りのサーバー処理はfallbackへstreamされる状態」と説明しています。つまり、すべてのデータ取得を速くする機能ではなく、待つべき場所とすぐ見せる場所を分ける設計 です。

今回押さえる要素は次の4つです。

要素役割実務での使いどころ
cacheComponents: trueCache Componentsを有効化静的Shellや"use cache"の検証
partialPrefetching: trueLinkごとにShellをprefetch一覧から詳細への初速改善
<Suspense>未キャッシュ処理をfallbackへ逃がすFirestoreの詳細取得や監査ログ取得
Navigation InspectorShellに何が含まれるか可視化画面遷移の見た目を検査

事前準備

16.3 Previewは next@preview として公開されています。npmのdist-tagでも preview が存在することを確認できます。モリソンのプロジェクト標準に合わせ、検証コマンドは yarn で書きます。

git switch -c chore/nextjs-instant-navigation
yarn add next@preview

次に next.config.ts でフラグを有効にします。公式ブログでは cacheComponentspartialPrefetching がこの機能群の前提として示されています。

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

export default nextConfig;

既存プロジェクトでWebpack固有設定、独自Babel設定、古いApp Routerの回避策を持っている場合は、まず yarn build が通るかだけを確認してください。Preview検証では、機能追加より先に「戻せるブランチで試す」ことが重要です。

ハンズオン1: 注文詳細をShellと動的データに分ける

OrderBoardでは、orders/{orderId} に注文概要、orders/{orderId}/items に明細、orders/{orderId}/auditLogs に監査ログがある想定にします。遷移直後に必要なのは、画面タイトル、注文番号、ステータスです。明細や監査ログは少し遅れても業務上は困りません。

ページ側では、すぐ表示したい概要と、待ってよい領域を分離します。

// src/app/orders/[id]/page.tsx
import { Suspense } from "react";
import { OrderAuditLog } from "./OrderAuditLog";
import { OrderItems } from "./OrderItems";
import { getOrderSummary } from "@/lib/orders/getOrderSummary";

type Props = PageProps<"/orders/[id]">;

export default function OrderDetailPage(props: Props) {
  return (
    <main className="space-y-6">
      <Suspense fallback={<p>注文概要を確認しています...</p>}>
        <OrderSummary params={props.params} />
      </Suspense>

      <Suspense fallback={<p>明細を読み込んでいます...</p>}>
        <OrderItems params={props.params} />
      </Suspense>

      <Suspense fallback={<p>監査ログを読み込んでいます...</p>}>
        <OrderAuditLog params={props.params} />
      </Suspense>
    </main>
  );
}

async function OrderSummary({ params }: { params: Props["params"] }) {
  const { id } = await params;
  const order = await getOrderSummary(id);

  return (
    <section>
      <p className="text-sm text-gray-500">注文番号: {order.code}</p>
      <h2 className="text-2xl font-bold">{order.customerName}</h2>
      <p>ステータス: {order.status}</p>
    </section>
  );
}

ポイントは、Firestore取得を全部ページ直下で await しないことです。<Suspense> の内側へ移すことで、Shellが先に表示され、明細や監査ログは後からstreamできます。

ハンズオン2: キャッシュしてよいFirestore取得を明示する

注文概要が頻繁に変わる場合はキャッシュ対象にできません。一方で、注文作成後にほぼ変わらない顧客名、注文番号、配送先ラベルなどはキャッシュ候補になります。Next.js 16系のCache Componentsでは、キャッシュしたい関数の先頭に "use cache" を置きます。

// src/lib/orders/getOrderSummary.ts
import { getFirestore } from "firebase-admin/firestore";

export type OrderSummary = {
  code: string;
  customerName: string;
  status: "draft" | "confirmed" | "shipped" | "cancelled";
};

export async function getOrderSummary(id: string): Promise<OrderSummary> {
  "use cache";

  const snapshot = await getFirestore().collection("orders").doc(id).get();

  if (!snapshot.exists) {
    throw new Error("Order not found");
  }

  const data = snapshot.data() as OrderSummary;
  return {
    code: data.code,
    customerName: data.customerName,
    status: data.status,
  };
}

ここで注意したいのは、キャッシュは都合の悪い遅さを隠す道具ではない という点です。ステータスが頻繁に変わる業務なら、status をキャッシュ対象から外す、再検証タイミングを設計する、またはその領域をSuspenseの動的取得に分離するほうが安全です。

ハンズオン3: 一覧のLinkでPrefetchを深くする

Partial Prefetchingを有効にすると、表示中の <Link> は宛先のApp Shellをprefetchします。さらに特定リンクでは prefetch を明示して、Shellだけでなくページコンテンツ側のprefetchも狙えます。

// src/app/orders/OrderList.tsx
import Link from "next/link";

type OrderRow = {
  id: string;
  code: string;
  customerName: string;
};

export function OrderList({ orders }: { orders: OrderRow[] }) {
  return (
    <ul className="divide-y">
      {orders.map((order) => (
        <li key={order.id} className="py-3">
          <Link href={`/orders/${order.id}`} prefetch className="block">
            <span className="font-medium">{order.code}</span>
            <span className="ml-3 text-gray-500">{order.customerName}</span>
          </Link>
        </li>
      ))}
    </ul>
  );
}

ただし、全リンクで深いprefetchを行うと、サーバー負荷やFirestore読み取り回数が増えます。業務画面では次のように優先順位を付けるのが現実的です。

画面prefetch方針理由
注文一覧の上位20件有効化を検討クリック率が高い
検索結果の全ページ慎重に検討表示件数が多く読み取りが増えやすい
管理者向け監査ログShellのみで十分詳細確認は頻度が低い

ハンズオン4: CIで遷移体験の退化を検知する

公式Docsでは、@next/playwrightinstant() helperで、navigation直後に見えるUIだけを検査できると説明されています。重要な導線だけでもE2Eに入れておくと、「誰かがページ直下に重いFirestore取得を足して遷移が止まった」問題を検知しやすくなります。

yarn add -D @next/playwright @playwright/test
// e2e/order-navigation.test.ts
import { expect, test } from "@playwright/test";
import { instant } from "@next/playwright";

test("注文詳細の概要が遷移直後に見える", async ({ page }) => {
  await page.goto("/orders");

  await instant(page, async () => {
    await page.click('a[href="/orders/demo-order-001"]');
    await expect(page.getByText("注文番号: OD-001")).toBeVisible();
  });

  await expect(page.getByText("明細を読み込んでいます...")).toBeVisible();
});

GitHub Actionsでは、まずビルドを必須にします。E2Eは環境変数やFirestore Emulatorの準備が必要なため、導入する場合は別jobに分けると運用しやすいです。

# .github/workflows/web-ci.yml
name: Web CI

on:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: yarn
      - run: yarn install --frozen-lockfile
      - run: yarn build

導入時の判断基準

Instant Navigationsは、すべてのルートに一括適用するより、まずユーザーが何度も往復する導線から試すのが向いています。OrderBoardなら「注文一覧 -> 注文詳細 -> 戻る」、CRMなら「顧客一覧 -> 顧客詳細」、在庫管理なら「商品一覧 -> 在庫詳細」です。

Preview段階では、次のチェックリストで扱う範囲を絞りましょう。

  • next@preview を本番に入れず、検証ブランチまたは検証環境で試す
  • Navigation InspectorでShellに出る情報を確認する
  • Firestore読み取りがprefetchで増えすぎないか見る
  • キャッシュしてよいデータと、常に最新であるべきデータを分ける
  • 重要導線だけ instant() でE2E化する

参考にした一次情報

まとめ

Next.js 16.3 PreviewのInstant Navigationsは、Firestoreを使う業務画面で特に効きやすい機能です。重要なのは、遷移を「全データ取得完了まで待つ処理」ではなく、「Shell、キャッシュ済み概要、Suspenseでstreamする詳細」に分け直すことです。

まずは注文詳細や顧客詳細のような往復頻度の高い1画面で、cacheComponentspartialPrefetching<Suspense>、Navigation Inspectorを試してみてください。ユーザーがクリックした瞬間に意味のある画面が見えるだけで、同じFirestore読み取りでも体感速度は大きく変わります。