🅿️

Next.js 14で追加されたPartial Prerenderingを試してみた

2023/11/26に公開

はじめに

Next.js 14より、Partial Prerenderingが実験的な機能として追加されました。
https://nextjs.org/blog/next-14

こちらの機能について、全体像が理解できていなかったので、自分で触って試してみました。

注意事項

  • 実験的な機能のため、現在のところ本番環境への利用は推奨されていません。
  • クライアントナビゲーション(SPA遷移)には現在未対応で、将来的には対応予定のようです。

概要

まずはVercelが提供しているデモを見てみましょう。

https://www.partialprerendering.com/

動的なコンテンツが非同期で取得されていることは確認できますが、具体的に何がどうプリレンダリングされているかは、ブラウザからは分かりづらいです。

次に文章を読み解いていきます。

上記のブログでは、Partial Prerenderingの説明として、以下のような記述があります。

Fast initial static response + streaming dynamic content

高速な初期静的応答 + 動的コンテンツのストリーミング

この内容を理解するためには、まずNext.jsのApp Routerでのレンダリング方式を考慮する必要がありそうです。

以下のドキュメントにレンダリング方式に関する記載があります。

https://nextjs.org/docs/app/building-your-application/rendering/server-components

特に大事な部分として、この中のDynamic Renderingのセクションには、以下のような記述があります。

Switching to Dynamic Rendering

During rendering, if a dynamic function or uncached data request is discovered, Next.js will switch to dynamically rendering the whole route.

レンダリング中に、動的関数またはキャッシュされていないデータ要求が検出された場合、Next.js はルート全体の動的レンダリングに切り替えます。

これは、ページを構成するコンポーネントの中で、動的に作用する要素を検出した際には、部分的に動的なレンダリングになるわけではなく、ページ全体が動的なレンダリングとなるということです。

つまりほとんどのコンポーネントが静的にレンダリング可能な場合でも、動的なデータが一部含まれるだけで、全体が動的にレンダリングされる挙動になります。これはパフォーマンス観点でよろしくありません。

これを解消して、静的にレンダリングできる部分はビルド時に処理してしまって、動的な部分のみ後から処理しましょう、というのがPartial Prerenderingです。

利用方法

オプトインで利用する機能となるので、まずはコンフィグからフラグをオンにしましょう。
https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: true,
  },
}
 
module.exports = nextConfig

Partial Prerenderingは、新規のAPIなどを提供しているわけではないため、コードについては特別書き換える必要はありません。特定のユースケースにおいて有効的に機能します。

具体的には以下のようなケース[1]です。

page.tsx
import { Suspense } from 'react';
import { Reviews, ReviewsSkeleton } from '#/components/reviews';

export default function Page() {
  return (
    <div className="space-y-8 lg:space-y-14">
      Static Rendered
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>
    </div>
  );
}
reviews.tsx
import type { Review } from '#/types/review';
import { ProductReviewCard } from '#/components/product-review-card';
import { delayReviews } from '#/lib/constants';

export async function Reviews() {
  let reviews: Review[] = await fetch(
    `https://app-router-api.vercel.app/api/reviews?delay=${delayReviews}`,
    {
      cache: 'no-store',
    },
  ).then((res) => res.json());

  return (
    <div className="space-y-6">
      <div className="text-lg font-medium text-white">Customer Reviews</div>
      <div className="space-y-8">
        {reviews.map((review) => {
          return <ProductReviewCard key={review.id} review={review} />;
        })}
      </div>
    </div>
  );
}

この場合、Reviewsコンポーネントの中では、cache: 'no-store'を指定したデータフェッチが行われているため、このコンポーネントは動的レンダリングになり、ページ全体も動的レンダリングとなります。

実際にこのコードをPartial Prerenderingをオフの状態でビルドすると、以下のようになります。
Partial Prerenderingオフ

該当のルートは、λ (Dynamic)と表示されています。
こちらをPartial Prerenderingをオンにしてビルドしてみます。

Partial Prerenderingオン

該当のルートは、◐ (Partial Prerender) と言う表記に変わってますね。

実際にビルドされた結果についても見てみましょう。
Partial Prerenderをオンにした場合、.next/server/app/に、設定オフの時は存在しなかったindex.htmlなどの、静的なファイルが作成されていることを確認できました。

$ ls .next/server/app/ | grep index
index.html
index.meta
index.prefetch.rsc

ファイルの中身もコンポーネント内の静的な要素の文字列がしっかりと含まれています。

index.html
<div class="space-y-8 lg:space-y-14">Static Rendered<!--$?-->

感想

Partial Prerenderingは、静的に事前生成できる部分はなるべく行おうという、正当な進化だと思いました。エッジから配信することができるため、グローバルなプロジェクトではメリットが大きそうです。

一方でドキュメントにも記載がありますが、Node.js runtime向けの機能となっていて、Edge Runtimeで動的にレンダリングしても高速なレスポンスできる場合は、特に使う必要はないということのようです。

Partial Prerendering is designed for the Node.js runtime only. Using the subset of the Node.js runtime is not needed when you can instantly serve the static shell.

https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering

今回のアップデートは、Server Actionsの追加などとは異なり、ユーザーが特別対応を考える必要のあるものではなさそうです。おそらくどこかのバージョンで、自動的に有効化されるのではないかと思います。

脚注
  1. Vercelが公開しているデモのコードを一部抜粋して利用しています。 ↩︎

Discussion