Next.js 15のApp Routerで高速なアプリを作る方法
以前、Next.js の App Router を使って新しいアプリを開発していた際、 App Router の画面遷移が Pages Router よりも遅く感じることがありました。そこで、なぜこのような違いが生じるのか、そして App Router の持つ機能がどのように画面遷移速度に影響するのかを理解するため、独自で調査を行いました。
デモ
テスト用にいくつかのサンプルページを作成しながら、App Router の各機能とそれらが画面遷移のパフォーマンスにどのように影響するかを示すためのデモアプリを開発しました。
▼デモアプリはこちら
https://nextperformancejp.vercel.app
App Router と Pages Router の比較
App RouterとPages Routerのパフォーマンスを比較すると、それぞれに特徴的な違いがあることがわかりました。デフォルトでは、Pages Router の方が画面遷移速度が速く、App Router は初回のページ読み込み速度がより高速だといえます。しかし、この初回のページ読み込み速度の差は、測定ツールがないと気付きにくいように感じます。
こうした違いが生じる主な理由は、2点あります。1点目は、Pages Router では他のページへ遷移する際に必要な JavaScript が初回のページ読み込み時にすでにブラウザにダウンロードされるということです。一方のApp Router は、ブラウザにダウンロードされる JavaScript が通常より小さくなっています。2点目は、App Router では動的コンテンツを初回リクエスト内でストリーム配信する仕様だということです。Pages Router では、これらが別々のリクエストになります。
動的コンテンツの表示
最初は、動的コンテンツを Suspense
でラップし、ローディング状態用のフォールバックを設定するだけで高速なナビゲーションが実現できると考えていました。しかし、デモアプリからも分かるように、それでは期待した結果が得られず、ナビゲーションは依然として少し遅いと感じられます。
export default function Page(props: { params: Promise<{ id: string }> }) {
return (
<div>
Static Title
+ <Suspense fallback={<Spinner />}>
<DynamicComponent {...props} />
+ </Suspense>
</div>
);
}
loading.tsx
をこのルートセグメントに追加すると、高速なナビゲーションが実現できます。しかし、これには新たな問題として、連続ローディングが発生してしまいます。その場合は、loading.tsx
のローディングが表示された後に、 Suspense
のフォールバックのローディングが表示されます。
ナビゲーションをさらに高速化するには、edge
ランタイムを使用する方法もあります。この方法では、ページコードに export const runtime = 'edge';
を追加するだけで、ページの読み込み速度が速くなります。ただし、edge
ランタイムはすべての Node.js 機能をサポートしているわけではなく、一部のライブラリが期待通りに動作しない場合があります。
キャッシュ
フロント側
フロント側のルーターキャッシュを有効にすることで、ユーザーが特定のページを再訪した際に高速なナビゲーションを実現できます。この動作は Next.js 14 ではデフォルトでしたが、Next.js 15 でこの機能を有効化するには、next.config.ts
ファイルに以下の記述を追加する必要があります。
const nextConfig: NextConfig = {
experimental: {
+ staleTimes: {
+ dynamic: 30,
+ static: 180,
+ },
},
};
これにより、Next.js はアクセスされた動的ページをフロント側で 30 秒間キャッシュします。ただし、このキャッシュは特定のユーザー専用であり、ページをリロードすると消えてしまいます。また、router.refresh()
を呼び出すことで、プログラム的にフロント側のキャッシュをクリアすることもできます。
サーバー側
動的コンテンツをサーバー側でキャッシュすることもできます。Suspense
でラップする場合と比較して、アプリのナビゲーション速度に直接影響を与えるわけではないものの、ローディング状態が表示されなくなるため、結果的にナビゲーションが高速化されます。
デモアプリでは unstable_cache()
を使用しましたが、新しい use cache
というトライアル機能を使用することもできます。
const getCachedDynamicData = unstable_cache(getDynamicData);
Partial Prerendering(PPR)
PPR をアプリで有効にすることで、1 つのページ内に静的コンテンツと動的コンテンツを共存させることが可能になります。この場合、事前にレンダリングされた静的なシェルがすぐに表示され、Suspense
でラップされている動的コンテンツは 、非同期的にロードされます。
現在、PPR を有効にするには最新の Next.js のcanaryバージョンが必要ですが、stable版にも導入されることを期待しています。その際には、ナビゲーションのパフォーマンスをあまり気にすることなく Next.js アプリを構築できるようになり、動的部分を Suspense
でラップするだけで高速なナビゲーションが実現できると考えています。
const nextConfig: NextConfig = {
+ experimental: {
+ ppr: true,
+ },
};
プリフェッチ
デフォルトでは、Next.js は画面上に表示されているリンクの静的コンテンツをすでにプリフェッチします。さらに、Link
コンポーネントに prefetch={true}
を設定することも可能です。これにより、Next.js はページ全体をプリフェッチし、動的部分も含めて即時のナビゲーションを実現できます。動的部分のローディングも表示されることはありません。
<Link href={link} prefetch>Link</Link>
もちろん、これにより不要なリクエストの数が増え、ホスティングサービスの帯域使用量が増加することで、サーバーに不要な負荷がかかる可能性もあります。
しかし、コンテンツをキャッシュすることでデータベースや外部サービスへのサーバーコールを減らすことができ、サーバー側の負荷を回避することが可能です。
他のトリック
このデモでは、アプリをどれほど高速化できるかを確認できます。
サーバーサイドのキャッシュやプリフェッチに加えて、このデモではナビゲーションをさらに高速化するための 2 つの工夫が施されています。1 つ目は、リンクにホバーした際にターゲットページの画像をプリフェッチすること、2 つ目はマウスボタンを離すのを待たずに、 onMouseDown
の時点でナビゲーションを開始することです。
これらのトリックは、 NextFaster プロジェクトから採用されました。主に確認すべきコードは、prefetch-images
のAPI ルートとカスタムの link.tsx
のコンポーネントです。
また、このプロジェクトの README ファイルにはコストの情報も記載されているので、ぜひチェックしてみてください。
結論
現時点では、App Router を使って Next.js アプリのナビゲーションを高速化するには、いくつかの工夫が必要です。しかし、PPR がstable版となれば、これをあまり意識しなくてもできるようになると思います。しかし、App Router にはサーバーコンポーネントからバックエンドコードを直接呼び出す機能、サーバーアクションやキャッシング、レイアウトといった便利な機能が数多く搭載されています。私自身も、今後のプロジェクトにおいて積極的に App Router を活用していく予定です。
Discussion