prerender
prerender は React ツリーを Web Stream を用いて静的な HTML 文字列にレンダーします。
const {prelude, postponed} = await prerender(reactNode, options?)リファレンス
prerender(reactNode, options?)
prerender を呼び出して、アプリを静的な HTML にレンダーします。
import { prerender } from 'react-dom/static';
async function handler(request, response) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}クライアント側では、このようにサーバ生成された HTML を操作可能にするために hydrateRoot を用います。
引数
-
reactNode: HTML へとレンダーしたい React ノード。例えば、<App />のような JSX 要素です。これはドキュメント全体を表すことが期待されているため、Appコンポーネントは<html>タグをレンダーする必要があります。 -
省略可能
options: 静的生成関連のオプションが含まれたオブジェクト。- 省略可能
bootstrapScriptContent: 指定された場合、この文字列がインラインの<script>タグ内に配置されます。 - 省略可能
bootstrapScripts: ページ上に出力する<script>タグに対応する URL 文字列の配列。これを使用して、hydrateRootを呼び出す<script>を含めます。クライアントで React をまったく実行したくない場合は省略します。 - 省略可能
bootstrapModules:bootstrapScriptsと同様ですが、代わりに<script type="module">を出力します。 - 省略可能
identifierPrefix: React がuseIdによって生成する ID に使用する文字列プレフィックス。同じページ上に複数のルートを使用する際に、競合を避けるために用います。hydrateRootにも同じプレフィックスを渡す必要があります。 - 省略可能
namespaceURI: このストリームのルートネームスペース URI 文字列。デフォルトでは通常の HTML です。SVG の場合は'http://www.w3.org/2000/svg'、MathML の場合は'http://www.w3.org/1998/Math/MathML'を渡します。 - 省略可能
onError: サーバエラーが発生するたびに発火するコールバック。復帰可能なエラーの場合もそうでないエラーの場合もあります。デフォルトではconsole.errorのみを呼び出します。これを上書きしてクラッシュレポートをログに記録する場合でもconsole.errorを呼び出すようにしてください。また、シェルが出力される前にステータスコードを調整するためにも使用できます。 - 省略可能
progressiveChunkSize: チャンクのバイト数。デフォルトの推論方法についてはこちらを参照してください。 - 省略可能
signal: プリレンダーを中止してクライアントで残りをレンダーするために使用できる abort signal。
- 省略可能
返り値
prerender はプロミスを返します。
- レンダーが成功した場合、プロミスは以下を含んだオブジェクトに解決 (resolve) されます。
prelude: HTML の Web Stream。このストリームを使ってレスポンスを送信したり、ストリームを文字列に一括して読み出したりできます。postponed:prerenderが終了しなかった場合には、resumeに渡すために用いる、JSON シリアライズ可能な非公開のオブジェクト。そうでない場合はnullで、これはpreduleに必要なすべてのコンテンツが入っておりresumeが必要ないことを表す。
- レンダーが失敗した場合は、Promise は拒否 (reject) されます。これを使用してフォールバックシェルを出力します。
注意点
プリレンダー中に nonce オプションは利用できません。nonce はリクエストごとに一意である必要があり、CSP でアプリケーションを保護するために nonce を使用する場合、プリレンダー自体に nonce 値を含めることは不適切かつ危険です。
使用法
React ツリーを静的な HTML としてストリームにレンダーする
prerender を呼び出して、React ツリーを静的な HTML として読み取り可能な Web Stream にレンダーします。
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}ルートコンポーネント と ブートストラップ <script> パスのリストを指定する必要があります。ルートコンポーネントは、ルートの <html> タグを含んだドキュメント全体を返すようにします。
例えば以下のような形になるでしょう。
export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}React は doctype とあなたが指定したブートストラップ <script> タグを結果の HTML ストリームに注入します。
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>クライアント側では、ブートストラップスクリプトは hydrateRoot を呼び出して document 全体のハイドレーションを行う必要があります。
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);これにより、サーバで生成された静的な HTML にイベントリスナが追加され、操作可能になります。
さらに深く知る
ビルド後に、最終的なアセット URL(JavaScript や CSS ファイルなど)にはよくハッシュ化が行われます。例えば、styles.css が styles.123456.css になることがあります。静的なアセットのファイル名をハッシュ化することで、同じアセットがビルドごとに異なるファイル名になることが保証されます。これが有用なのは、ある特定の名前を持つファイルの内容が不変になり、静的なアセットの長期的なキャッシングを安全に行えるようになるためです。
しかし、ビルド後までアセット URL が分からない場合、それらをソースコードに含めることができません。例えば、先ほどのように JSX に "/styles.css" をハードコーディングする方法は動作しません。ソースコードにそれらを含めないようにするため、ルートコンポーネントが、props 経由で渡されたマップから実際のファイル名を読み取るようにすることができます。
export default function App({ assetMap }) {
return (
<html>
<head>
<title>My app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}サーバ上では、<App assetMap={assetMap} /> のようにレンダーし、アセット URL を含む assetMap を渡します。
// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const {prelude} = await prerender(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}サーバで <App assetMap={assetMap} /> のようにレンダーしているので、クライアントでも assetMap を使ってレンダーしてハイドレーションエラーを避ける必要があります。このためには以下のように assetMap をシリアライズしてクライアントに渡します。
// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const {prelude} = await prerender(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}上記の例では、bootstrapScriptContent オプションを使って<script> タグを追加して、クライアント上でグローバル window.assetMap 変数をセットしています。これにより、クライアントのコードが同じ assetMap を読み取れるようになります。
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);クライアントとサーバの両方が props として同じ assetMap を使って App をレンダーするため、ハイドレーションエラーは発生しません。
React ツリーを静的な HTML 文字列にレンダーする
prerender を呼び出して、アプリを静的な HTML 文字列にレンダーします。
import { prerender } from 'react-dom/static';
async function renderToString() {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
const reader = prelude.getReader();
let content = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return content;
}
content += Buffer.from(value).toString('utf8');
}
}これにより、React コンポーネントの、操作できない初期 HTML が生成されます。クライアントでは、hydrateRoot を呼び出してこのサーバで生成された HTML に対するハイドレーションを行い、操作可能にする必要があります。
全データの読み込みを待機
prerender は、静的な HTML 生成を行って解決する前に、全データがロードされるのを待機します。例えば以下のようなプロフィールページがあり、カバー、フレンド・写真が含まれたサイドバー、投稿のリストを表示しているところを考えましょう。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}ここで、<Posts /> のデータを読み込むのに時間がかかるとしましょう。理想的には、投稿の読み込みを待機して、HTML に含めてしまいたいとします。これを実現するには、サスペンスを使ってそのデータをサスペンドします。prerender はそのサスペンドされているコンテンツの読み込みを待機してから、静的 HTML へと解決します。
プリレンダーの中止
プリレンダー処理は、一定時間経過 (timeout) 後に強制的に「諦めさせる」ことが可能です。
async function renderToString() {
const controller = new AbortController();
setTimeout(() => {
controller.abort()
}, 10000);
try {
// the prelude will contain all the HTML that was prerendered
// before the controller aborted.
const {prelude} = await prerender(<App />, {
signal: controller.signal,
});
//...サスペンスバウンダリは、子のレンダーが未完了の場合にはフォールバックの状態で結果 (prelude) に含まれます。
これは resume または resumeAndPrerender を用いた部分プリレンダリングで利用可能です。
トラブルシューティング
アプリ全体がレンダーされるまでストリームが始まらない
prerender の返り値は解決する前に、全サスペンスバウンダリが解決することも含む、アプリ全体のレンダーの終了を待機します。これは事前静的サイト生成 (SSG) のために設計されているものであり、コンテンツを読み込みながらのストリーミングをサポートしません。
コンテンツを読み込みながらストリームしたい場合は、サーバレンダー API である renderToReadableStream などを使用してください。