⚛️

PPR の紹介と実装【Next.js 16】

に公開

はじめに

先日、Next.js の勉強会で、PPR(Partial Prerendering)について取り上げました 🫐

静的なサイトの高速性を保ちながら、動的なコンテンツとの組み合わせを可能にする技術は、
モダンな Web アプリケーションにおいて非常に強力です。

今回は、Next.js 16 の PPR について調査したので、基礎的な内容をまとめました!
時間の節約になれば、嬉しいです 🙌

PPR とは?

https://nextjs.org/blog/next-16

PPR(Partial Prerendering)とは、
ページの一部を事前にレンダリングし、他の部分を動的に処理することで、パフォーマンスと柔軟性を両立させる機能です

従来のレンダリング手法では、ページ全体を静的にするか動的にするかの二択でしたが、
PPR では、同一ページ内で静的な部分と動的な部分を組み合わせることができます!

今までの Next.js のレンダリング手法のおさらい

⚫️ SSG(Static Site Generation)

  • ページ全体を静的にレンダリングする
  • npm run build でビルドするタイミングで、データフェッチなどを行い、HTML を生成
  • これにより、リクエストが来たときは、すでに HTML が生成されているため、高速に表示できる
  • しかし、データが古くなる可能性がある

👇 こういうこと

npm run build

ビルド結果を確認すると、以下のような出力が表示されます:

Route (app)
┌ ○ /
├ ○ /_not-found
└ ● /projects/[id]
  ├ /projects/1
  ├ /projects/2
  └ /projects/3


○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses generateStaticParams)

⚫️ SSR(Server-Side Rendering)

  • リクエストごとにレンダリングする
  • リクエストが来た時に、データフェッチなどを行い、HTML を生成
  • これにより、リクエストが来たときに、最新のデータを取得できる
  • しかし、毎回サーバー処理が必要で、SSG と比べると遅いです

👇 こういうこと

npm run build

ビルド結果を確認すると、以下のような出力が表示されます:

Route (app)
┌ ○ /
├ ○ /_not-found
└ ƒ /projects/[id]


○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

重要なことは、

  • 今までは、ページ全体を静的にするか動的にするかの二択でした。
  • PPR では、ページの大部分を静的に生成しながら、一部を動的にするという、ハイブリッドな手法が可能です!

PPR の利点

PPR の主な利点として、以下が挙げられます:

  • 初期ロード時間の短縮: 静的な部分を事前にレンダリングすることで、ページの表示が高速化
  • パーソナライゼーション: 動的な部分でユーザー固有のコンテンツを提供
  • SEO 最適化: 静的な部分は検索エンジンに適切にインデックスされる

静的レンダリングの高速性と動的レンダリングの柔軟性を、同時に活用できるので、便利です 👍

👇

npm run build

ビルド結果を確認すると:

Route (app)         Revalidate  Expire
┌ ○ /                      15m      1y
├ ○ /_not-found
└ ◐ /projects/[id]         15m      1y
  ├ /projects/[id]         15m      1y
  ├ /projects/1            15m      1y
  ├ /projects/2            15m      1y
  └ /projects/3            15m      1y


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

PPR の使い方

Next.js 16 では、PPR が Cache Components として統合され、
"use cache" ディレクティブを使用してコンポーネントレベルでキャッシュを制御できるようになりました。

実装方法について、詳しく見ていきましょう!

1: Cache Components の有効化

https://nextjs.org/docs/app/api-reference/directives/use-cache

まず、next.config.js ファイルで Cache Components を有効にする必要があります。

import type { NextConfig } from "next";

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

export default nextConfig;

この設定により、PPR の機能が利用可能になります!

2: "use cache" ディレクティブの使用

キャッシュしたいコンポーネントの先頭に "use cache"; ディレクティブを追加します。

// components/ProductInfo.tsx
"use cache";

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
}

const getProduct = async (id: string): Promise<Product> => {
  const response = await fetch(`https://api.example.com/products/${id}`);
  return response.json();
};

export default async function ProductInfo({
  productId,
}: {
  productId: string;
}) {
  const product = await getProduct(productId);

  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <p>価格: ¥{product.price.toLocaleString()}</p>
    </div>
  );
}

3: 静的部分と動的部分の組み合わせ

PPR の真価は、静的な部分と動的な部分を組み合わせた時に発揮されます:

// app/products/[id]/page.tsx
import ProductInfo from "@/components/ProductInfo";
import UserRecommendations from "@/components/UserRecommendations";
import { Suspense } from "react";

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <main>
      {/* 静的にキャッシュされる部分 */}
      <ProductInfo productId={id} />

      {/* 動的に生成される部分 */}
      <Suspense fallback={<div>Loading...</div>}>
        <UserRecommendations userId="current-user" />
      </Suspense>
    </main>
  );
}

この例では、商品情報は静的にキャッシュされ、ユーザーごとのおすすめ情報は動的に生成されます!

なぜ PPR なのか?

個人的に、Next.js 16 での、PPR と Cache Components の追加は、とても嬉しい変更です!

理由は主に2つ:

1. ハイブリッドなレンダリング手法が簡単になった

プロジェクト詳細ページ

上記の、「クラウドファンディングアプリ」の画像にある通り、
1つのページ内で、 あまり変更されない部分と、動的なデータを表示させたい。。 というケースは珍しくないです。

  • クラウドファンディングを行うプロジェクトの概要文は、あまり変更されない
  • 現在の支援状況は、できるだけリアルタイムで正確に表示したい

そのようなケースで、今までは、ページ全体を SSG にするか、SSR にするかの二択でした。

SSG を行って、クライアントサイドで動的なデータを取得する、という方法も可能ですが、
それだと、「どういう理由でサーバー/クライアントサイドで処理を実行するのかの区別」がわかりにくくなってきます

プロジェクトが複雑になるほど、上記が曖昧になり、判断の負荷が高くなりますね。
PPR は、これを簡単な記述で解決できます 👍

2. キャッシュの制御がより柔軟/明確になった

Next.js v15 までは、以下のような方法で、キャッシュが制御できました:

  • fetch 関数でキャッシュのオプションを指定する
  • ページレベルで revalidate や dynamic を指定する
  • Dynamic APIを使用する

個人的には、
このような複数の記述方法、そしてキャッシュの動作を把握するのが、少し大変だと感じることもありました。

(自分と同じように、Next.js のキャッシュ周りの設定に関して、少し複雑さを感じていた開発者も多いかと思います 😎)

Next.js 16 では、ページ/コンポーネント/関数のあらゆるレベルで、"use cache" ディレクティブを使用して、キャッシュを制御できます!

例:

const getProduct = async (id: string): Promise<Product> => {
  "use cache";
  cacheLife("hours"); // 1時間キャッシュ
  cacheTag("products"); // products タグでキャッシュを管理
  const response = await fetch(`https://api.example.com/products/${id}`);
  return response.json();
};

基本的に同じ"use cache"の記述で、設定できる点がシンプルです!

何より、制御したい処理の近くに、それぞれの設定を記述できるので、
キャッシュ設定の管理が、少し見やすくなった印象でもあります!

おわりに

最後まで読んでいただき、ありがとうございます 🥳

下記の、React コミュニティ(勉強会/共同開発)での、振り返りのような記事ですが、
少しでも参考になれば、嬉しいです!

https://b13o.com?utm_source=zenn&utm_medium=article&utm_campaign=about-ppr-nextjs

そして、もし、間違いや補足情報などがありましたら、
ぜひコメントを追加してください!

Happy Hacking :)

参考

https://nextjs.org/blog/next-16
https://nextjs.org/docs/app/api-reference/directives/use-cache
https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering
https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model

b13o Tech Blog

Discussion