静的サイトの生成 (SSG)
アーキテクチャでは、テーマが Webpack で実行されていることを述べました。 しかし、それは常にブラウザのグローバルにアクセスできるという意味ではないことに注意が必要です。 以下のように、テーマは2回ビルドされます。
- サーバサイドレンダリング中、テーマは React DOM Server 呼ばれるサンドボックス内でコンパイルされます。 これは
window
やdocument
がない「ヘッドレスブラウザ」として見ることができ、React のみです。 SSR は静的な HTML ページを生成します。 - クライアント側でのレンダリング中、テーマはブラウザで最終的に実行される JavaScript にコンパイルされ、ブラウザ変数にアクセスできます。
サーバサイドレンダリング (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'
といった実際の値に置き換えられます。 デッドコードの除去後、それぞれに応じたビルド結果を得るでしょう。
- 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}</>;
}
import React from 'react';
export default function expensiveComp() {
- if ('production' === '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を訪れたユーザーのブラウザ画面に最初に届くものです(ルーティングを参照のこと)。 Afterwards, the browser fetches and runs other JS code to provide the "dynamic" parts of your site—anything implemented with JavaScript. However, before that, the main content of your page is already visible, allowing faster loading.
In CSR-only apps, all DOM elements are generated on client side with React, and the HTML file only ever contains one root element for React to mount DOM to; in SSR, React is already facing a fully built HTML page, and it only needs to correlate the DOM elements with the virtual DOM in its model. This step is called "hydration". After React has hydrated the static markup, the app starts to work as any normal React app.
Note that Docusaurus is ultimately a single-page application, so static site generation is only an optimization (progressive enhancement, as it's called), but our functionality does not fully depend on those HTML files. This is contrary to site generators like Jekyll and Docusaurus v1, where all files are statically transformed to markup, and interactiveness is added through external JavaScript linked with <script>
tags. If you inspect the build output, you will still see JS assets under build/assets/js
, which are, really, the core of Docusaurus.
Escape hatches
If you want to render any dynamic content on your screen that relies on the browser API to be functional at all, for example:
- Our live codeblock, which runs in the browser's JS runtime
- Our themed image that detects the user's color scheme to display different images
- The JSON viewer of our debug panel which uses the
window
global for styling
You may need to escape from SSR since static HTML can't display anything useful without knowing the client state.
It is important for the first client-side render to produce the exact same DOM structure as server-side rendering, otherwise, React will correlate virtual DOM with the wrong DOM elements.
Therefore, the naïve attempt of if (typeof window !== 'undefined) {/* render something */}
won't work appropriately as a browser vs. server detection, because the first client render would instantly render different markup from the server-generated one.
You can read more about this pitfall in The Perils of Rehydration.
We provide several more reliable ways to escape SSR.
<BrowserOnly>
If you need to render some component in browser only (for example, because the component relies on browser specifics to be functional at all), one common approach is to wrap your component with <BrowserOnly>
to make sure it's invisible during SSR and only rendered in 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>
);
}
It's important to realize that the children of <BrowserOnly>
is not a JSX element, but a function that returns an element. This is a design decision. Consider this code:
import BrowserOnly from '@docusaurus/BrowserOnly';
function MyComponent() {
return (
<BrowserOnly>
{/* このように書かないでください(動きません) */}
<span>page url = {window.location.href}</span>
</BrowserOnly>
);
}
While you may expect that BrowserOnly
hides away the children during server-side rendering, it actually can't. When the React renderer tries to render this JSX tree, it does see the {window.location.href}
variable as a node of this tree and tries to render it, although it's actually not used! Using a function ensures that we only let the renderer see the browser-only component when it's needed.
useIsBrowser
You can also use the useIsBrowser()
hook to test if the component is currently in a browser environment. It returns false
in SSR and true
is CSR, after first client render. Use this hook if you only need to perform certain conditional operations on client-side, but not render an entirely different UI.
import useIsBrowser from '@docusaurus/useIsBrowser';
function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
}
useEffect
Lastly, you can put your logic in useEffect()
to delay its execution until after first CSR. This is most appropriate if you are only performing side-effects but don't get data from the client state.
function MyComponent() {
useEffect(() => {
// Only logged in the browser console; nothing is logged during server-side rendering
console.log("I'm now in the browser");
}, []);
return <span>Some content...</span>;
}
ExecutionEnvironment
The ExecutionEnvironment
namespace contains several values, and canUseDOM
is an effective way to detect browser environment.
Beware that it essentially checked typeof window !== 'undefined'
under the hood, so you should not use it for rendering-related logic, but only imperative code, like reacting to user input by sending web requests, or dynamically importing libraries, where DOM isn't updated at all.
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
document.title = "ロード完了!";
}