Astro + svelte でSSRをがっつり使う設計を試した
TL;DR
MPAはシンプルで良い。ランタイムのjsが小さいことは正義。
Astroは意外と黒魔術的。
基本的にAstroで書く。そんなにjsは必要ではない。
ハイドレーションする箇所を増えてくるとクライアント側のパフォーマンスが厳しい。本末転倒。
再利用性を高めてコンポーネント化しはじめると、コンポーネントをAstroで作るべきかSvelteで作るべきかが悩ましくなる。
構成
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される。
Astroの実現方法:
Astroコンポーネントは基本的にサーバーでのみ評価される。Framework Component で Svelte など任意の描画フレームワークをAstroコンポーネント内に置ける。SSRされ、hydrationもされる。
<SvelteComponent client:load /> をつけると、そのコンポーネントから先がhydrationされる。
ここで面白いのが、同じコンポーネントであってもhydrationするか否か選べる。つまりhydrationしないと使えないステートを含むコンポーネントであっても、hydrationしないという選択肢が取れる。たとえばPropsによってはstateを使わないことが自明なケースなど、hydrationさせないことで最適化できる。いい意味で自由、悪い意味で事故る。
個人的には結構好きだが、完全性は失われている。
サーバー側で評価される描画領域の一部分を遅延してクライアント側に送信する。
メリット:
評価が終わった部分から逐次ダウンロードすることによって、時間のかかる部分に律速されずに描画できる。
Next.js の実現方法:
Server Component の Suspense 境界によって、先に評価された部分がクライアント側で描画され、評価に時間がかかったコンポーネントはあとからストリーミングされてきて描画される。
ストリーミングで1リクエストになっている点が面白い。
面白いのは Suspense で括る領域によって、どこまで描画を遅延させるかを選べる点。
Astro の実現方法:
Server Island
サーバー側で評価される描画領域について、ランタイムで描画する領域を削減する。
メリット:
描画領域を部分的に静的化しキャッシュすることで、サーバー側のjsの評価量を削減。つまりTTFB減少。メモリなどのサーバー側のリソース削減にもなる。
さらにリソースの問い合わせした結果も含めて静的化してキャッシュすることで大幅な最適化。
Next.js の実現方法:
Partial Pre-Rendering (PPR) によって実現。貪欲に静的化したいコンポーネントをマークしつつ、Suspense 境界によって明示的に動的な部分を囲う。静的化した部分をキャッシュしていくことで、オンデマンドなレンダリングを求められるコンポーネントが存在したとしても、部分的に最適化を効かせることができる。
(要調査) ビルドの時点で revalidate されても変化しないと分かっている描画領域は静的に埋め込んでいる?
React.createElementのツリーを作ったりするコストの削減。
Suspense 境界を利用して遅延してクライアント側に送る点は単純な Server Component と変わらないが、さらに評価結果をキャッシュするかどうかを明示できる点で、柔軟な最適化ができるようになっている。
これはすごく React っぽい考え方で、表示するときは一貫して全体を再評価する設計のシンプルさを保ちつつ、キャッシュによってそのコストを削減することができる。
これは初期の React の思想から一貫していて、状態の変化に対して一貫してrenderツリーを評価しdiffing によって意味のないDOM操作を減らすという最初の発想から変わっていない。Refreshによってサーバーから一貫してすべてをリクエストし、描画を構築し直すというそのプロセスを Suspense の多段化によってコストの掛かる部分をキャッシュしていくのだ。すごい。
Astro の実現方法:
AstroコンポーネントはJSXの文法で記述するが、基本的にただのテンプレートエンジン。ビルド時にすべて文字列に変数を埋め込むだけのランタイムになる。
しかし大きな違いはサーバー側とクライアント側の境界の明快さだと思う。
Nextはその構成上、ロジックがマークアップに載りやすい。そしてクライアント側の動作ロジックも混在できる。これは実装の柔軟性ではメリットでもあるが、境界が曖昧になることで設計が複雑性が増すデメリットでもある。
書き途中。