サーバーサイドレンダリング (Server-Side Rendering)
デフォルトでは、Yewコンポーネントはクライアントサイドでレンダリングされます。ユーザーがウェブサイトにアクセスすると、サーバーは実際のコンテンツを含まない骨組みのHTMLファイルとWebAssemblyパッケージをブラウザに送信します。すべてのコンテンツはクライアントサイドでWebAssemblyパッケージによってレンダリングされます。これをクライアントサイドレンダリングと呼びます。
この方法はほとんどのウェブサイトにとって有効ですが、いくつかの注意点があります:
- ユーザーはWebAssemblyパッケージがダウンロードされ、初期レンダリングが完了するまで何も表示されません。これにより、ネットワークが遅い場合にユーザーエクスペリエンスが悪化する可能性があります。
- 一部の検索エンジンは動的にレンダリングされたウェブページのコンテンツをサポートしておらず、サポートしている検索エンジンでも通常は動的なウェブサイトのランキングが低くなります。
これらの問題を解決するために、ウェブサイトをサーバーサイドでレンダリングすることができます。
動作原理
Yewはページをサーバーサイドでレンダリングするための ServerRenderer
を提供しています。
Yewコンポーネントをサーバーサイドでレンダリングするには、ServerRenderer::<App>::new()
を使用してレンダラーを作成し、renderer.render().await
を呼び出して <App />
を String
としてレンダリングします。
use yew::prelude::*;
use yew::ServerRenderer;
#[function_component]
fn App() -> Html {
html! {<div>{"Hello, World!"}</div>}
}
// この例が CI の WASM 環境で動作することを保証するために `flavor = "current_thread"` を使用しています。
// マルチスレッドを使用したい場合は、デフォルトの `#[tokio::main]` マクロを使用できます。
#[tokio::main(flavor = "current_thread")]
async fn no_main() {
let renderer = ServerRenderer::<App>::new();
let rendered = renderer.render().await;
// プリント: <div>Hello, World!</div>
println!("{}", rendered);
}
コンポーネントのライフサイクル
クライアントサイドレンダリングとは異なり、サーバーサイドレンダリング時のコンポーネントのライフサイクルは異なります。
コンポーネントが最初に Html
として正常にレンダリングされるまで、use_effect
(および use_effect_with
)以外のすべてのフックは正常に動作します。
web_sys
などのブラウザ関連のインターフェースは、サーバーサイドレンダリング時には利用できません。これらを使用しようとすると、アプリケーションがクラッシュします。このロジックは use_effect
または use_effect_with
に隔離する必要があります。これらはサーバーサイドレンダリング時には実行されないためです。
サーバーサイドレンダリング時に構造化コンポーネントを使用することは可能ですが、クライアントサイドの安全なロジック(関数コンポーネントの use_effect
フックなど)とライフサイクルイベントの間には明確な境界がなく、ライフサイクルイベントの呼び出し順序もクライアントとは異なります。
さらに、構造化コンポーネントは、すべての子コンポーネントがレンダリングされ destroy
メソッドが呼び出されるまでメッセージを受け取り続けます。開発者は、コンポーネントに渡される可能性のあるメッセージがブラウザインターフェースを呼び出すロジックにリンクされないようにする必要があります。
サーバーサイドレンダリングをサポートするアプリケーションを設計する際は、特別な理由がない限り、関数コンポーネントを使用することをお勧めします。
サーバーサイドレンダリング中のデータ取得
データ取得はサーバーサイドレンダリングとハイドレーション(hydration)中の難点の一つです。
従来の方法では、コンポーネントがレンダリングされるとすぐに利用可能になります(仮想DOMを出力してレンダリングします)。コンポーネントがデータを取得する必要がない場合、この方法は有効です。しかし、コンポーネントがレンダリング時にデータを取得しようとするとどうなるでしょうか?
以前は、Yewにはコンポーネントがまだデータを取得しているかどうかを検出するメカニズムがありませんでした。データ取得クライアントは、初期レンダリング中に何が要求されたかを検出し、要求が完了した後に再レンダリングをトリガーするソリューションを実装する責任がありました。サーバーはこのプロセスを繰り返し、応答を返す前にレンダリング中に追加の保留中の要求がないことを確認します。
これは、コンポーネントを繰り返しレンダリングするため、CPUリソースを浪費するだけでなく、データクライアントは、サーバー側で取得したデータをハイドレーション中に利用可能にする方法を提供する必要があり、初期レンダリングで返される仮想DOMがサーバーサイドレンダリングのDOMツリーと一致することを保証する必要があります。これは実現が難しい場合があります。
Yewは、<Suspense />
を使用してこの問題を解決する異なるアプローチを採用しています。
<Suspense />
は特別なコンポーネントで、クライアント側で使用する場合、コンポーネントがデータを取得(保留)している間にフォールバックUIを表示し、データ取得が完了した後に通常のUIに戻る方法を提供します。
アプリケーションがサーバーサイドレンダリングされると、Yewはコンポーネントが保留状態でなくなるまで待機し、それを文字列バッファにシリアル化します。
ハイドレーション中、<Suspense />
コンポーネント内の要素は、すべての子コンポーネントが保留状態でなくなるまでハイドレーションされません。
この方法により、開発者はサーバーサイドレンダリングに対応したクライアント非依存のアプリケーションを簡単に構築し、データ取得を行うことができます。
SSR ハイドレーション
サーバーサイドレンダリングハイドレーション(SSR Hydration)
ハイドレーションは、Yewアプリケーションをサーバー側で生成されたHTMLファイルに接続するプロセスです。デフォルトでは、ServerRender
はハイドレーション可能なHTML文字列を出力し、追加情報を含んでハイドレーションを容易にします。Renderer::hydrate
メソッドを呼び出すと、Yewは最初からレンダリングするのではなく、アプリケーションが生成した仮想DOMとサーバーレンダラーが生成したHTML文字列を調整します。
ServerRenderer
が作成したHTMLマークアップを正常にハイドレーションするためには、クライアントはSSRに使用されたレイアウトと完全に一致する仮想DOMレイアウトを生成する必要があります。要素を含まないコンポーネントも含めてです。特定の実装でのみ使用されるコンポーネントがある場合は、PhantomComponent
を使用して追加のコンポーネントの位置を埋めることを検討してください。
SSR出力(静的HTML)をブラウザが初期レンダリングした後、実際のDOMが期待されるDOMと一致する場合にのみ、ハイドレーションは成功します。HTMLが規格に準拠していない場合、ハイドレーションは失敗する可能性があります。ブラウザは不正なHTMLのDOM構造を変更する可能性があり、実際のDOMが期待されるDOMと異なることがあります。例えば、<tbody>
のない <table>
がある場合、ブラウザはDOMに <tbody>
を追加する可能性があります。
ハイドレーション中のコンポーネントライフサイクル
ハイドレーション中、コンポーネントは作成後に2回連続してレンダリングされます。すべてのエフェクトは2回目のレンダリングが完了した後に呼び出されます。コンポーネントのレンダリング関数に副作用がないことを確認することが重要です。状態を変更したり、追加のレンダリングをトリガーしたりしないようにしてください。現在、状態を変更したり追加のレンダリングをトリガーしたりするコンポーネントがある場合は、それらを use_effect
フックに移動してください。
ハイドレーション中、構造化コンポーネントを使用してサーバーサイドレンダリングを行うことができます。ビュー関数はレンダリング関数の前に複数回呼び出されます。レンダリング関数が呼び出されるまで、DOMは未接続と見なされ、rendered()
メソッドが呼び出される前にレンダリングノードにアクセスすることを防ぐ必要があります。
例
use yew::prelude::*;
use yew::Renderer;
#[function_component]
fn App() -> Html {
html! {<div>{"Hello, World!"}</div>}
}
fn main() {
let renderer = Renderer::<App>::new();
// body 要素の下のすべてのコンテンツをハイドレーションし、末尾の要素を削除します(存在する場合)。
renderer.hydrate();
}
例: simple_ssr 例: ssr_router
シングルスレッドモード
Yewは yew::LocalServerRenderer
を使用してシングルスレッドでのサーバーサイドレンダリングをサポートしています。このモードはWASIのようなシングルスレッド環境に適しています。
// Rustc 1.78以降では、`wasm32-wasip1` または `wasm32-wasip2` ターゲットを使用してビルドします。
// 古いバージョンのRustc(1.84以前)を使用している場合は、`wasm32-wasi` ターゲットを使用してビルドすることもできます。
// 詳細については、https://blog.rust-lang.org/2024/04/09/updates-to-rusts-wasi-targets.html を参照してください。
use yew::prelude::*;
use yew::LocalServerRenderer;
#[function_component]
fn App() -> Html {
use yew_router::prelude::*;
html! {
<>
<h1>{"Yew WASI SSR demo"}</h1>
</>
}
}
pub async fn render() -> String {
let renderer = LocalServerRenderer::<App>::new();
let html_raw = renderer.render().await;
let mut body = String::new();
body.push_str("<body>");
body.push_str("<div id='app'>");
body.push_str(&html_raw);
body.push_str("</div>");
body.push_str("</body>");
body
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("{}", render().await);
}
wasm32-unknown-unknown
ターゲットを使用してSSRアプリケーションをビルドする場合、not_browser_env
機能フラグを使用して、Yew内部のブラウザ固有のAPIへのアクセスを無効にすることができます。これは、Cloudflare Workerのようなサーバーレスプラットフォームで非常に便利です。
サーバーサイドレンダリングは現在実験的な機能です。バグを見つけた場合は、GitHubで報告してください。