Astro + svelte でSSRをがっつり使う設計を試した
TL;DR
MPAはシンプルで良い。ランタイムのjsが小さいことは正義。
Astroは意外と黒魔術的。
基本的にAstroで書く。そんなにjsは必要ではない。
ハイドレーションする箇所を増えてくるとクライアント側のパフォーマンスが厳しい。本末転倒。
再利用性を高めてコンポーネント化しはじめると、コンポーネントをAstroで作るべきかSvelteで作るべきかが悩ましくなる。
hydration ポイントは Astro と Svelte の境界でしか定義できないので、いちど hydration しないと決めた Svelte コンポーネントの内部で hydration したくなったときが悩ましい。
AIに書き換えてもらうことがしばしば。わりと一対一で変換できるのでちょいチューニングが必要(svelte4を吐き出さないようにする)だが、わりと素直に行ける。
構成
Astro (一部静的出力はあるが大部分はSSR)
svelte (クライアントコンポーネントとして)
typescriptで可能な限り型で保護する
gRPCでのスキーマファーストなAPI設計
APIサーバーはそれ単体で独立
対象
閲覧が支配的
UGC
しかしログイン後に動的要素もまあまあある
いいねなどのアクション、検索ボックス、編集系のフォーム、など
Astroを選択した理由は、単純な構造の高速なMPAを構成しつつ、動的要素も不自由なく載せられると考えたためである。単純さは正義。そして世の中はそこまで思っているほど動的ではない。EC, Blog, SNS、SSRが求められるようなコンテンツファーストなWebアプリケーションほど大部分が静的である。
極論、単純にコンテンツを出すだけであればmustacheテンプレートに変数埋め込むくらいでも良いと考えている。できるだけレンダリングは単純な方が良い。API側に描画のため必要なデータを加工するロジックを寄せて、レンダリングはただマークアップの構造を作るだけの責務が理想的。それに加えて動的に描画する分はhtmlをサーバーで描画して送ってもらいましょう。クライアント側でマークアップを組み上げる時代は終わりです。まさに時代はルネッサンス。閑話休題。
ところが場合によってはリッチな状態変化を含むアプリを組み込む部分もある。検索ボックスや投稿フォームなど。これを両立しやすいのがAstroのUIフレームワークを埋め込める機構。
NextでもServer Componentによって、サーバー側でのみ評価されるコンポーネントとクライアント側でも実行されるコンポーネントと作れるようになった。いくつかの機能をAstroとNext.jsとを比較してみよう。
サーバーでのみ評価される描画領域と、クライアント側でのみ評価される描画領域を分離できる。
メリット:
一般的なすべてをhydrationするSSRと比較して、クライアント側で完全なhydarationをする領域を減らせるのでクライアント側のjs削減。Download time減少。評価量も減少するので、TTIも減少。
今回のケースではインタラクティブな要素がほとんど無いので、ほぼほぼSSRだけ。hydrationはほんの一部ということができれば理想的である。
サーバーの描画領域単位で必要なリソースに問い合わせる設計によって、関心のあるロジックをコンポーネント内部に閉じ込めることができる。
これは枝葉のコンポーネントでリソースに問い合わせることになり、最適化の難易度を上げるデメリットにもなる。
Next.jsの実現方法:
Server Component はサーバーでのみ評価される。Client Component は今まで通りSSRされ、hydrationされる。
"use client"; をつけるとそのファイルに含まれるコンポーネントから先がhydrationされる。
(厳密にはすべての React の ツリーが構築されるが、完全に静的なのでシリアライズされたツリー構造が単純に再構築される。クライアントでのルーティングではそのシリアライズされたツリー構造を受け取る。)
Astroの実現方法:
Astroコンポーネントは基本的にサーバーでのみ評価される。Framework Component で Svelte など任意の描画フレームワークをAstroコンポーネント内に置ける。SSRされ、hydrationもされる。
面白いのは Astro に React や Svelte など複数のクライアントコンポーネントを埋め込んでそれぞれ hydaration できる。なんというキメラ。
<SvelteComponent client:load /> をつけると、そのコンポーネントから先がhydrationされる。
ここで面白いのが、同じコンポーネントであってもhydrationするか否か選べる。つまりhydrationしないと使えないステートを含むコンポーネントであっても、hydrationしないという選択肢が取れる。たとえばPropsによってはstateを使わないことが自明なケースなど、hydrationさせないことで最適化できる。いい意味で自由、悪い意味で事故る。
個人的には結構好きだが、完全性は失われている。
Nextと違う点はサーバーで描画された部分は単純にhtmlとして配信されるのみ。そのためMPAとなる。
サーバー側で評価される描画領域の一部分を遅延してクライアント側に送信する。
メリット:
評価が終わった部分から逐次ダウンロードすることによって、時間のかかる部分に律速されずに完了した部分から描画できる。
Next.js の実現方法:
Server Component の Suspense 境界によって、先に評価された部分がクライアント側で描画され、評価に時間がかかったコンポーネントはあとからストリーミングされてきて描画される。
ストリーミングで1リクエストになっている点が面白い。
特徴的なのは Suspense で括る領域によって、どこまで描画を遅延させるかを選べる点。
Astro の実現方法:
Server Island を使う。Astroのコンポーネントをマウントするときに、server:defer ディレクティブをつけて遅延するかどうかを選べる。
Nextと違う点は、リクエストの戦略。初期レンダリングが終わった後にクライアント側からfetchが走る。
単純にリクエストをキャッシュすることも可能なのはシンプルで良い。
サーバー側で評価される描画領域について、ランタイムで描画する領域を削減する。
メリット:
描画領域を部分的に静的化しキャッシュすることで、サーバー側のjsの評価量を削減。つまりTTFB(Time to first byte)減少。メモリなどのサーバー側のリソース削減にもなる。
さらにリソースの問い合わせした結果も含めて静的化してキャッシュすることで大幅な最適化。
Next.js の実現方法:
Partial Pre-Rendering (PPR) によって実現。貪欲に静的化したいコンポーネントをマークしつつ、Suspense 境界によって明示的に動的な部分を囲う。静的化した部分をキャッシュしていくことで、オンデマンドなレンダリング(毎リクエストで再描画)を求められるコンポーネントが存在したとしても、部分的に最適化を効かせることができる。
(要調査) ビルドの時点で revalidate されても変化しないと分かっている描画領域は静的に埋め込んでいる?
React.createElement のツリーを構築するコストの削減。
Suspense 境界を利用して遅延してクライアント側に送る点は単純な Server Component と変わらないが、さらに評価結果をキャッシュするかどうかを明示できる点で、柔軟な最適化ができるようになっている。
これはすごく React っぽい考え方で、表示するときは一貫して全体を再評価する設計のシンプルさを保ちつつ、キャッシュによってそのコストを削減することができる。
これは初期の React の思想から一貫していて、状態の変化に対して一貫して render ツリーを評価し diffing によって意味のないDOM操作を減らすという最初の発想から変わっていない。Refresh によってサーバーから一貫してすべてをリクエストし、描画を構築し直すというそのプロセスを Suspense の多段化によってコストの掛かる部分をキャッシュし削減していくのだ。すごい。
Astro の実現方法:
Astro コンポーネントは JSX の文法で記述するが、基本的にただのテンプレートエンジン。ビルド時にすべて文字列に変数を埋め込むだけのランタイムになる。
これは単にレンダリングのコストをビルド時に最適化するというテクニック。
この思想がかなり好き。シンプルでよい。
NextのPPRのビルド時に静的化している動作とほぼ同等(要調査)。PPRはオンデマンドなレンダリングをキャッシュしてコスト削減するテクニックなので、さらに適用範囲は広く変数を含んだ部分までコスト削減できる。
Astro には Next の revalidatePath 相当のキャッシュ機構はない。
いろいろ比較してみたが、多くは同等な機能がある。Nextはそれを暗黙的に行っており、Astroはわりと明確である。たとえば、クライアントとサーバーの境界などは非常に明確
Nextはその構成上、ロジックがマークアップに載りやすい。そしてクライアント側の動作ロジックも混在できる。これは実装の柔軟性ではメリットでもあるが、境界が曖昧になることで設計が複雑性が増すデメリットでもある。
書き途中。