Skip to main content

静的サイトの生成 (SSG)

アーキテクチャでは、テーマが Webpack で実行されていることを述べました。 しかし、それは常にブラウザのグローバルにアクセスできるという意味ではないことに注意が必要です。 以下のように、テーマは2回ビルドされます。

  • サーバサイドレンダリング中、テーマは React DOM Server 呼ばれるサンドボックス内でコンパイルされます。 これは windowdocumentがない「ヘッドレスブラウザ」として見ることができ、React のみです。 SSR は静的な HTML ページを生成します。
  • クライアント側でのレンダリング中、テーマはブラウザで最終的に実行される JavaScript にコンパイルされ、ブラウザ変数にアクセスできます。
SSR か SSG か?

サーバサイドレンダリング (SSR)静的サイトの生成 (SSG) は異なる概念である場合がありますが、私たちはこの2つを相互に使用します。

厳密に言えば、Docusaurus は静的サイトジェネレータです。 サーバサイドのランタイムがないため、リクエストごとに動的に事前にレンダリングするのではなく、CDN にデプロイされる HTML ファイルに静的にレンダリングします。 これは Next.js の動作モデルとは異なります。

したがって、process (例外あり) や 'fs' モジュールといった Node のグローバル変数にアクセスできないことは知っているかもしれませんが、ブラウザのグローバル変数にも自由にアクセスすることはできません。

import React from 'react';

export default function WhereAmI() {
return <span>{window.location.href}</span>;
}

これは慣用的な React のように見えますが、 docusaurus build を実行すると次のエラーが表示されます。

ReferenceError: window is not defined

これはサーバサイドレンダリング中、実際には Docusaurus アプリがブラウザで実行されず、window が何であるかがわからないためです。

process.env.NODE_ENV は?

process.env.NODE_ENV は、Node のグローバル変数にアクセスできない原則の例外です。 実際、この変数は React で使用できます。Webpack がこの変数をグローバルなものとして注入するためです。

import React from 'react';

export default function expensiveComp() {
if (process.env.NODE_ENV === 'development') {
return <>This component is not shown in development</>;
}
const res = someExpensiveOperationThatLastsALongTime();
return <>{res}</>;
}

Webpack によるビルド中、process.env.NODE_ENV'development' または 'production' といった実際の値に置き換えられます。 デッドコードの除去後、それぞれに応じたビルド結果を得るでしょう。

import React from 'react';

export default function expensiveComp() {
if ('development' === 'development') {
+ return <>This component is not shown in development</>;
}
- const res = someExpensiveOperationThatLastsALongTime();
- return <>{res}</>;
}

SSR を理解する

React は単なる動的な UI ランタイムではなく、テンプレートエンジンでもあります。 Docusaurus サイトはたいてい静的なコンテンツを含んでいるため、(React を実行するような) JavaScript なしで、プレーンな HTML/CSS のみで動作する必要があります。 React コードを動的なコンテンツなしで静的に HTML にレンダリングすることができるのも、サーバサイドレンダリングによるものです。 HTML ファイルはクライアントの状態という概念を持たないため (つまり純粋なマークアップです)、ブラウザの API に依存するべきではありません。

これらのHTMLファイルは、URLを訪れたユーザーのブラウザ画面に最初に届くものです(ルーティングを参照のこと)。 ブラウザはその後、他のJavaScriptコードを取得して実行し、サイトの「動的」な部分—JavaScriptで実装されたすべての機能—を提供します。 しかし、その前にページの主要なコンテンツはすでに表示されているため、より高速な読み込みが可能になっています。

CSR専用のアプリでは、すべてのDOM要素がクライアント側でReactによって生成され、HTMLファイルにはReactがマウントするためのルート要素のみが含まれます。一方SSRでは、Reactは既に構築された完全なHTMLページを受け取り、そのDOM要素を自身の仮想DOMと照合するだけで済みます。 この処理は「ハイドレーション」と呼ばれます。 Reactが静的マークアップのハイドレーションを完了すると、アプリは通常のReactアプリとして動作を開始します。

Docusaurusはシングルページアプリケーション(SPA)であるため、静的サイト生成はあくまで最適化手法の一つ(いわゆる_プログレッシブエンハンスメント_)であり、機能はそれらのHTMLファイルに完全に依存しているわけではありません。 これは、すべてのファイルが静的にマークアップに変換され、インタラクティブな機能は <script> タグで読み込まれる外部JavaScriptによって追加される、JekyllDocusaurus v1 のようなサイトジェネレーターとは異なります。 ビルド出力を確認すると、build/assets/js 以下にJavaScriptアセットが存在しているのがわかります。これらこそがDocusaurusの核となる部分です。

エスケープハッチ(抜け道・回避策)

もし、画面上でブラウザAPIに依存した動的コンテンツをレンダリングしたい場合、例えば:

  • ライブコードブロックはブラウザーのJSランタイム上で実行されます
  • ユーザーのカラースキームを検出し、異なる画像を表示するテーマ画像
  • スタイリングに window グローバルオブジェクトを使用している、デバッグパネルのJSONビューワー

クライアントの状態を把握できなければ、静的なHTMLでは有用な表示ができないため、SSR(サーバーサイドレンダリング)から抜け出す必要がある場合があります。

警告

最初のクライアント側レンダリングがサーバーサイドレンダリングとまったく同じDOM構造を生成することが重要です。そうでないと、Reactが仮想DOMと誤ったDOM要素を関連付けてしまいます。

そのため、if (typeof window !== 'undefined') {/* render something */} のような単純なブラウザ/サーバー判定は適切に機能しません。最初のクライアントレンダー時にサーバー生成のマークアップと異なるものがすぐにレンダリングされてしまうからです。

この落とし穴については、The Perils of Rehydration で詳しく解説されています。

SSRから抜け出すための、より信頼性の高い方法をいくつか用意しています。

<BrowserOnly>

もし、あるコンポーネントがブラウザ特有の機能に依存していて、ブラウザ上でのみレンダリングしたい場合、一般的な方法としてそのコンポーネントを<BrowserOnly>でラップします。これにより、サーバーサイドレンダリング時は非表示になり、CSR(クライアントサイド)でのみレンダリングされます。

import BrowserOnly from '@docusaurus/BrowserOnly';

function MyComponent(props) {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
const LibComponent =
require('some-lib-that-accesses-window').LibComponent;
return <LibComponent {...props} />;
}}
</BrowserOnly>
);
}

<BrowserOnly> の子要素はJSX要素ではなく、要素を_返す_関数であることを理解しておくことが重要です。 これは設計上の判断です。 以下のコードを考えてみましょう:

import BrowserOnly from '@docusaurus/BrowserOnly';

function MyComponent() {
return (
<BrowserOnly>
{/* このように書かないでください(動きません) */}
<span>page url = {window.location.href}</span>
</BrowserOnly>
);
}

BrowserOnly がサーバーサイドレンダリング中に子要素を隠すと予想されがちですが、実際にはそれはできません。 ReactのレンダラーがこのJSXツリーをレンダリングしようとすると、{window.location.href} という変数もツリーの一部として認識し、実際には使われていなくてもレンダリングしようとします。 関数を使うことで、レンダラーにブラウザ専用コンポーネントを必要なときだけ認識させることができます。

useIsBrowser

useIsBrowser() フックを使うことで、コンポーネントが現在ブラウザ環境で動作しているかどうかを判定できます。 このフックは、SSR(サーバーサイドレンダリング)時には false を返し、最初のクライアントレンダリング後のCSR(クライアントサイドレンダリング)時には true を返します。 このフックは、クライアント側でのみ特定の条件付き処理を行いたい場合に使い、まったく異なるUIをレンダリングする必要がないときに適しています。

import useIsBrowser from '@docusaurus/useIsBrowser';

function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
}

useEffect

最後に、処理を useEffect() の中に記述することで、初回のクライアントサイドレンダリング後まで実行を遅延させることができます。 これは副作用のみを実行し、クライアントの状態からデータを_取得しない_場合に最も適しています。

function MyComponent() {
useEffect(() => {
// ブラウザのコンソールにのみログが出力され、サーバーサイドレンダリング時は何もログされません
console.log("現在ブラウザ上で実行されています");
}, []);
return <span>いくつかのコンテンツ...</span>;
}

ExecutionEnvironment

ExecutionEnvironment 名前空間にはいくつかの値が含まれており、canUseDOM はブラウザ環境を検出する有効な方法です。

canUseDOMは内部的に基本的に typeof window !== 'undefined' をチェックしているため、レンダリングに関わるロジックには使わず、ユーザー入力に応じたウェブリクエストの送信や、ライブラリの動的インポートなど、DOMの更新を伴わない命令的なコードにのみ使用すべきです。

a-client-module.js
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

if (ExecutionEnvironment.canUseDOM) {
document.title = "ロード完了!";
}