🏃

SSRのレンダリング方法から見るSolidStartとNext.jsの基本戦略の違い

に公開

注意書き

以下の記事は SolidStart1.1.3 と Next.js15 の比較になります。
内容の正確性について一切保証するものではありません。

記事の趣旨

React.jsとSolid.jsは、文法的によく似ていると言われます。また根本的な設計思想の違いが取り沙汰されます。
それでは、それぞれから派生したSSRフレームワークであるNext.js と SolidStartはどう違うのでしょうか?
今回はSSRのレンダリング・ハイドレーション周りの違いからそれを考察したいと思います。

Next.jsと似て非なる"use server"

SolidStartはRPCを中心に据えている

Next.jsと同様 SolidStartにも"use server"というディレクティブがあるのですが若干意味が違います。

Next.js
Server Actionの関数定義 OR
(クライアントコンポーネントの中にある)コンポーネントをサーバコンポーネントにするよ、という目印
SolidStart
この関数(または、このファイルに書かれた関数)をRPCとして公開してクライアント側からアクセスできるようにするよ、という目印

です。SolidStartの場合はブラウザのformのsubmit機能と結びついたメソッドだけでなく、Serovalというライブラリでシリアライズ、デシリアライズ可能なデータ型をやり取りする限りはあらゆる関数をRPC化できる魔法の言葉になっています。

SolidStartではサーバーコンポーネント、クライアントコンポーネントという概念はありません。
実際この後見ていきますが、SSRのレンダリングに関係するところでもこのSolidStart独特の"use server"という関数をRPC化する呪文が大活躍します。

https://github.com/lxsmnsyc/seroval/tree/main
プライベートな情報にアクセスできる関数を"use server"するときは認可処理を必ず挟んだ方がいいと思われます。

名前は同じなのに働きが逆 Suspense

選べるSSRモード

Next.js(のサーバーコンポーネント)では、ストリーリングSSR を実現したい時、<Suspense>タグで囲むと思います。逆に サーバサイドで動的コンテンツを先に描画 したいときは <Suspense> で囲まないようにすると思います。が、

※SolidStartではその逆になります。サーバサイドで<AsyncComponent>までレンダリングしたい場合
(繰り返しになりますが、SSRのモードを sync asyncにした場合です。)
以下のように記述します。

const getUser = query(async () => {
  "use server";
  return { name: "Taro" };
}, "user");

function AsyncContent() {
  const user = createAsync(() => getUser());
  return <div>Hello, {user()?.name}</div>;
}

export default function Page() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <AsyncContent />
    </Suspense>
  );
}

SolidStartのレンダリング、ハイドレーションのタイミングはチャンク受信ごとに一括

SolidStartは脳筋的なレンダリング戦略を採用

以下は実際のコーディングにはあまり関係ない内部機構の話になります。

クライアントサイドのハイドレーション、レンダリングは単純で、一発目のシェルHTML受信 -> ハイドレーション、レンダリング -> <Suspense>境界内のHTMLが届き次第順次部分的にレンダリング、ハイドレーションを行う。

という感じらしいです。一方でNext.jsの<Suspense>のレンダリング・ハイドレーション機構はConcurrency RenderingやSelective Hydration という仕組みに乗っかっていて複雑になっています。Concurrency Renderingは、<Suspense>境界内のレンダリングの作業を細切れにして優先順位を調整したり、途中で一時停止したり再開したりする機構です。Selective Hydrationはハイドレーションの優先順位を調整する機構です。
なぜNext.jsでこんな仕組みが必要なのかというとNext.jsが基盤とするReactはcoarse-grained(粗粒度) reactivityという思想で、レンダリング、ハイドレーション自体のコストが高いからです。これは大雑把に言って、<Suspense>境界内で依存する状態が変化するたびに<Suspense>境界内部全体を再レンダリングする必要が出てきます。これを愚直に実行するとUXが低下します。

一方、Solid.jsはfine-grained(細粒度) reactivity と言って、状態変化に依存するDOMをピンポイントで変化させます。またハイドレーションのコストも既存のsignalにDOMを結びつけるだけなので、コストが低いです。
なのでSolidStartでは チャンクごとに一括 という単純な戦略を採用してるものと思われます。

結局Next.jsとSolidStart何が違うのか

少ない開発工数でUXを向上させるという基本命題にどう向き合うか

基本的にSPAではなくSSRフレームワークを導入するのにはいろいろな理由がありますが、一番大きな理由は開発工数の削減とUX向上です。

これまでみてきた通り、Next.jsとSolidStartにはいろいろな違いがあります。

SSRのレンダリング関係でいうと以下のようにまとめられるかもしれません。

FW 構成要素 個々の要素の機能性 FWの戦略(どうやって高い生産性と機能性を実現している?)
Next.js ServerComponent ConcurrencyRendering SelectiveHydration 複雑 洗練された機能を独断的にユーザに使わせる
SolidStart RPC Signal SSRモード 単純 単純な機能の組み合わせ

Next.jsは洗練された構成要素を使いUXを極限まで追求することで業界のデファクトになりました。
一方SolidStartはオープンで単純な機能(場合によっては他のOSS)の組み合わせでこれをを実現している分、若干Next.jsレベルには達していない部分がある気がします。一方その分、オープンに開発が進められているのでベンダーロックインなどの心配は少ないです。
また、Solid.js自体の機構が単純でReact.jsよりも学習カーブが緩やかであると言うメリットがあります。

まだまだこの記事では拾いきれなかった点はたくさんあると思いますが、ご容赦ください。

Discussion