React/Next.js/Remix に見られる、この頃のアーキテクチャの変化をキャッチアップすべく、横断的に紹介する本連載。今回はレンダリングアーキテクチャに注目します。それぞれの特徴を押さえながら、実際に動かすところまで学んでみましょう。
React 19 にて追加された機能にReact Server Componentsもありますという話は前回書きました。今回はそのReact Server Componentsを中心にレンダリングにまつわる話を書いていきます。
といってもReact Server ComponentsはReact 19ではじめて登場した概念ではなく、2020年には「Introducing Zero-Bundle-Size React Server Components」というタイトルの記事がビデオとともに公開されています。
Next.jsはいち早くReact Server Componentsの仕様を実装してきたフレームワークでした。Next.jsがバージョン13以降で React Server Componentsをベースとした構築スタイルであるApp Routerを採用して以降、新しいレンダリング形式への移行を進めてきました。
まずは、Next.jsの現状のレンダリング方法をおさらいしつつ、React Server Componentを体験し、学んでいきましょう。
レンダリングアーキテクチチャのおさらい
React が登場してからというもの、
- CSR (Client Side Rendering)
- SSR (Server Side Rendering)
- SSG (Static Site Generation:静的サイトジェネレーター)
などさまざまなレンダリングスタイルが登場してきましたが、近年Next.jsで追加されたものにISRがあり、その発展形としてPPR があります。
前述の3点については、さまざまな場所ですでに語られつくしていると思うので、今回はISRとPPRについて見ていきましょう。
ISR (Incremental Static Regeneration)
ISRは、CSR(Client Side Rendering)やSSR(Server Side Rendering)などの「なんとかRendering」とつく用語の略かと思いきや全然違う略語で、Incremental Static Regenerationの略です。直訳された「増分静的再生成」を日本語のブログなどで見かけることもあります。
ISRはNext.jsでサポートされている機能で、いままでアクセスされたことがないページにアクセスされた際は動的にページを表示し、それ以降のアクセスはキャッシュされたページをブラウザに返すというスタイルです。また、一定時間後にアクセスされた際にページの再生成を行い、再生成が完了次第キャッシュを置き換えます。
つまり、SSGのようにビルド時に全ページを生成してしまうわけでもなく、SSRのように都度サーバー側でレンダリングをしていくというわけでもないスタイルです。とはいえ毎回ではなくともサーバーサイドでレンダリングするというプロセス自体は発生するため、処理を行うためのサーバーは必要となりますが、静的生成のメリットも持ち合わせたアーキテクチャです。
このISRの仕組みを発展させたものとして登場したのが次に紹介するPPRです。
PPR (Partial Prerendering)
PPR(Partial Prerendering)はNext.js 14で Previewになり、Next.js 15でexperimentalになった機能です。したがって、仕様が変更される可能性のある機能ですが、執筆時点では検証環境にて試していくことができます。今のところ next@canaryで利用することができます。
PPRは日本語で言うと「部分的な事前レンダリング」です。ISRとSSRの統合モデルであるとドキュメント上ではうたわれています。
今までのレンダリング形式がページ単位によるコントロールが基本だったのに対して、PPRでは、ページの静的部分をまずは事前レンダリングしておき、動的なエリアについてはサーバー側から後追いで送信してくるコンポーネント情報を利用して表示を行います。後追いといっても、事前レンダリングの部分を含めてすべて単一のHTTPリクエストで実行されるという点が特徴的です。
ちなみに、部分的な動的エリアについては、ReactのSuspenseという機能を使うことで Next.jsに指定をすることができ、最初にページにアクセスしたときは「ロード中」などといった表示をしておくことができます。
以下の図を見てください。
この図では、ページの大部分は静的な要素となっています。一部だけバックエンドと連携しているなどの可能性があるダイナミックな要素となっています。PPRを使うことで一部分だけダイナミックなサーバーコンポーネントにすることが可能になるのです。
実際にPPRで作成した場合のページサンプルは Next.js Partial Prerenderingによってブラウザで確認することができます。ページ全体は先に表示され、動的な要素を含んだ一部分だけ後から描画されるのがわかるでしょう。
React Server Component(RSC)自体を学ぶ
Next.js以外にも他のフレームワークによるReact Server Component(RSCと略して呼ばれることもあります)の採用事例が存在します。それらは似た部分もあれば異なる部分もあるため、各フレームワークの個別実装を学習する前に、RSC自体がどういうものなのか、どういう役割なのかということを理解しておくことが重要だと考えます。したがって今回は、それぞれのフレームワークを学習するときに、より長く使えるベースとなるRSCを一緒に見ていきましょう。
さて、RSCとは一体どういう技術なのでしょうか。React のドキュメントから読み解いていきましょう。もし時間があればRSCのRFC(request for comments)もセットで読み込んでいくと背景などがよりわかると思います。
RSCは、サーバー上で実行されるコンポーネントです。それだとSSRと同じじゃないか、と思われるかもしれません。確かにSSRもサーバー側でレンダリングされた内容がブラウザに返されるため、サーバー上で一度レンダリングされることになります。
ただ、SSRされたコンテンツはHTMLがクライアントに返されるの対し、RSCで扱うサーバーコンポーネントはサーバー上で実行され、必要なレンダリングが行われた後にクライアント側に段階的に送られ(ストリーミングという表現がされています)クライアント側ではサーバーコンポーネントが結合されて表示されます。
■SRCのメリット1:シンプルな記述で「読み込み中」を表示しておける
段階的にブラウザにコンポーネントのレンダリングを行えるメリットとして、ReactのSuspenseを使って、まだロードできていないサーバーコンポーネントについては「読み込み中」などの表示を出すことも可能となります。
■SRCのメリット2:あらかじめクライアント側のダウンロードがいらない
SSRはサーバー側で実行されたコンポーネントも比較や画面遷移のためにクライアント側で保持している必要があるため、初回アクセス時に多くのコンポーネントをあらかじめダウンロードしておく必要があります。
一方、サーバーコンポーネントはクライアント側にコンポーネントのコードを保持しておく必要がないというメリットがあります。(あらかじめクライアント側にダウンロードしておく必要がないことからゼロバンドルと呼ばれたりします)
■SRCのメリット3:サーバー側の処理をつかえる
加えて、サーバーコンポーネントは必要に応じてサーバー側の資産(データベースや各種API、処理が重いライブラリなど)にアクセスでき、サーバー側で処理できます。サーバーコンポーネントはクライアントコンポーネントを含めることもでき、その場合は必要に応じてインタラクティブな処理を記述することもできます。このクライアントコンポーネントが持っている状態は、サーバーコンポーネントが何らかの理由で再ロードされた場合でも状態を保持することができます。
ここまで読むと、PPRの解説と重なるところがあるなと思った方もいるでしょう。Next.jsが React Server Component を採用したことで、このような仕組みを実現可能になったのです。
PPRを通してReact Server Componentを体験してみる
Next.jsはReact Server Componentの仕様をすべて実装しています。そのため、React Server Componentを体験するには、Next.jsの最新版を使うのが手っ取り早いです。まず、前回作成した簡易バックエンドに以下のコードを追加しましょう。
追加するのはユーザーの一覧を返すという想定のAPIです。 http://localhost:4000/api/users
でJSONを取得できるようにしていきます。
まずは、返すユーザーの一覧の情報を定義します。本来はデータベースから一覧を取得しているものですが、ここでは簡易的に直接書いてしまいます。
続いてGET
メソッドでユーザーの一覧を取得するためのAPIを定義します。
src/app.js
...
const users = [
{ id: 1, name: "Kurt Cobain" },
{ id: 2, name: "Eddie Vedder" },
];
...
app.get("/api/users", (req, res) => {
// ウェイトとして1秒待つ処理
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
res.json(users);
});
コードの途中で登場する見慣れないAtomic.wait()
はテスト用に用意した、少し時間をおいてレスポンスを返すためのウェイト処理です。詳細は割愛しますが、興味があればMDNのサイトをチェックしてみてください。
これでバックエンド部分の記述は完了です。
前回作成したフロントエンドを改修していきましょう。まずは、APIよりデータを取得するための関数を記述します。これが先ほど作成したバックエンドへアクセスするための関数です。
src/app/data.ts
...
export interface User {
id: string;
name: string;
}
export const getUsers = async (): Promise<User[]> => {
const res = await fetch("http://localhost:4000/api/users");
return await res.json();
};
続いて、ユーザー一覧を表示するコンポーネントを以下のように作成します。このファイルには、ユーザーの一覧を表示する入れ物となるコンポーネントUserList
と、ユーザー一覧を表示するコンポーネントUserListDetail
があります。
いずれもサーバーコンポーネントとして作成しますが、PPRのダイナミックコンポーネントの境界としてSuspense
でUserListDetail
コンポーネントをラッピングしています。想定が正しければ、ユーザーが最初にアクセスしたときは「Loading…」が表示されており、3秒後にユーザー一覧が表示されるはずです。
src/app/UserList.tsx
import { Suspense } from "react";
import { User, getUsers } from "./data";
import { UserListDetail } from "./user";
export const UserList = () => {
return (
<Suspense fallback={<p>Loading...</p>}>
<UserListDetail />
</Suspense>
);
};
export const UserListDetail = async () => {
const users = await getUsers();
return (
<ul className="ml-8 list-disc">
{users?.map((user: User) => (
<li key={user?.id}>{user?.name}</li>
))}
</ul>
);
};
つづいてトップページを改修しましょう。PPRを有効にするためのexport const experimental_ppr = true;
を記述し、Home
関数の中にUserList
コンポーネントを読み込むように以下のとおりに改修します。
src/app/page.tsx
import { UserForm } from "./user";
import { UserList } from "./UserList";
export const experimental_ppr = true;
export default function Home() {
return (
<div className="p-6">
<h1 className="font-bold mt-4 mb-2 text-2xl">Users</h1>
<h2 className="font-bold mt-4 mb-2">Add user</h2>
<UserForm></UserForm>
<h2 className="font-bold mt-4 mb-2">User List</h2>
<UserList></UserList>
</div>
);
}
最後に設定ファイルにてPPR向けのフラグを設定します。
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
ppr: 'incremental',
}
};
export default nextConfig;
これで簡単なサンプルではありますが、PPRを体験できるサイトが出来上がりました。起動すると、3秒後にUser Listが表示されるはずです。
実際、Chrome の開発ツールのNetworkタブで確認すると、HTMLの取得は1リクエストで完結しているのがわかります。
このページのHTMLのソースを見てみると、最初は該当箇所には、以下のコードしかありませんでした。
...
<!--$?-->
<template id="B:0"></template>
<p>Loading...</p>
<!--/$-->
...
しかし、ロードが完了すると、さまざまな要素が追加されているのがわかります。ユーザー一覧のシリアライズされたコンポーネントとレンダリング済みのHTMLも確認できます。ただ、このあたりの内部データ構造は今後変わる可能性もあるので、あくまで現時点での挙動の確認にとどめておきましょう。
...
<script>
self.__next_f.push([1, "8:[\"$\",\"ul\",null,{\"className\":\"ml-8 list-disc\",\"children\":[[\"$\",\"li\",\"1\",{\"children\":\"Kurt Cobain\"}],[\"$\",\"li\",\"2\",{\"children\":\"Eddie Vedder\"}]]}]\n"])
</script>
...
...
<div hidden id="S:0">
<ul class="ml-8 list-disc">
<li>Kurt Cobain</li>
<li>Eddie Vedder</li>
</ul>
</div>
...
他のフレームワークにおけるReact Server Componentの対応
※React Server Componentを利用した機能はBeta やCanary版として提供されている場合があります。導入に際し注意してください。
■GatsbyとReact Server Component
GatsbyはReactの「React プロジェクトを始める」ページでも紹介されているSSG(Static Site Generator)です。GatsbyもReact Server Componentの対応を進めています。Gatsby上では、「Partial Hydration」という呼び方で利用しています。
Partial Hydrationは下図のように、Gatsby の静的書き出しの仕組みの上で部分的な動的なコンポーネントの読み込みを実現しています。
以下のページにも記載があるとおり、Gatsbyはバージョン5にてPartial Hydrationの仕組みを導入し、PARTIAL_HYDRATION
フラグをtrue
にすることで機能させることができる状態になっています。
■RedwoodJS
RedwoodJS は、React、GraphQL、Prisma、TypeScript、Jest、Storybook などで構成されているWebアプリケーションフレームワークです。Prismaの採用でもわかるとおりフロントエンドのフレームワークではなく、フルスタックなWebアプリケーションフレームワークです。RedwoodJS でもReact Server Componentの導入が積極的に進行中です。
バックエンドの処理を書くこともできる RedwoodJSではよりReact Server Componentの効果を感じられると思います。
Creating Your Own Appのセクションで紹介されているcreate-redwood-appのCanaryビルドを使ってプロジェクトを生成することで、React Server Componentが有効になった状態を確認することができます。サンプルページでは、サーバーコンポーネントの部分とクライアントコンポーネントの部分を色分けして表示してある様子を確認できます。
今回は、React Server Componetを中心にフレームワークの持つレンダリング周りのアップデートについて解説しました。
次回は、ファイルベースルーティングの今、そして旧バージョンからのマイグレーション方法について見ていきましょう。
▼React / Next.js / Remix にみるこの頃のアーキテクチャ変化
1:React 19 のアップデートにみる変化
2:レンダリングアーキテクチャの変化
3:フロントエンドルーティング定義の変化