Communitygithub.com

Svelte 5 サーバーサイドレンダリング(ルーン)

ルーン($state, $derived, $effect, $props, $bindable)を使用した Svelte 5 アプリの構築、移行、またはレビューを行います。SvelteKit SSR(adapter-auto/node/cloudflare/vercel/netlify)またはプレーンな Vite SSR(svelte/server の render() と hydrate() を使用)をサポートします。

Svelte 5 サーバーサイドレンダリング(ルーン) とは?

このスキルは、Svelte 5 のルーンを使用したサーバーサイドレンダリング(SSR)アプリケーションの構築、移行、およびレビューを支援します。主に SvelteKit(各種アダプター対応)とプレーンな Vite SSR の2つのプロジェクト形状をサポートし、それぞれのセットアップ手順を提供します。SSR において重要なのは、ルーンがサーバー上で実行されるかどうかを理解することです。例えば、$effect はサーバーでは決して実行されないため、それに依存する状態は SSR 時に欠落します。また、モジュールレベルの $state はユーザー間で共有されるリークの原因となるため、リクエストごとにスコープされた状態管理(コンテキストや load 関数の使用)が推奨されます。さらに、ハイドレーションの契約を守り、Date.now() や Math.random() などの非決定的な値を使用しないことで、ハイドレーション不一致を防ぎます。セキュリティ面では、シークレットの漏洩防止、CSP ノンス、XSS 対策、CSRF 保護など、包括的なルールを提供します。このスキルは、既存アプリの監査やデバッグにも使用でき、参照ファイル(runes-ssr.md, ssr-build.md, ssr-review.md, security.md)と組み合わせることで、詳細な知識と手順を提供します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agentmemoir/skills/tree/HEAD/skills/frontend/svelte5-ssr-runes

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Svelte 5 サーバーサイドレンダリング(ルーン) は何をしますか?

このスキルがカバーする内容

「SSR」とは、すべてのリクエストがサーバー(Node、Bun、サーバーレス、またはエッジ)によって HTML にレンダリングされ、ブラウザがそれを受け取った後、同じコンポーネントがハイドレートされてインタラクティブなアプリになることを意味します。静的なビルドとは異なり、リクエスト時に実際のサーバーが存在するため、+page.server.js、+layout.server.js、+server.js、フォームアクション、hooks.server.js が許可されます。これらはデータとシークレットの置き場所です。

ルーンの部分が SSR を難しくしています。すべてのコンポーネントはサーバーとブラウザの2回実行されます。クライアント専用アプリで問題ないコードでも、ここでは壊れます:

  • $effect はサーバーでは決して実行されません。そのため、それが設定する状態はサーバーの HTML から欠落します。
  • .svelte.ts ファイル内のモジュールレベルの $state は、サーバープロセスごとに1回作成され、すべてのユーザーのリクエスト間で共有されます。
  • レンダリング中に Date.now()、Math.random()、localStorage から計算された値はサーバーとクライアントで異なり、ハイドレーション不一致を引き起こします。

これらの3つの失敗が、ルーンコードにおけるほとんどの SSR バグの原因です。それぞれに以下の小さな正しいパターンがあります。

ステップ1 — プロジェクトの形状を選ぶ

ニーズ使用するもの
フルアプリ:ルーティング、サーバーデータ読み込み、フォーム、認証、ストリーミングSvelteKit + adapter-auto(Vercel/Netlify/Cloudflare 自動検出)、adapter-node(セルフホスト)、またはプラットフォームアダプター。これがデフォルトです。理由がない限りこれを使用してください。
既存のバックエンド(Express/Hono/Fastify/Bun)内の Svelte コンポーネントツリープレーンな Vite + Svelte 5:サーバーでは svelte/server の render()、ブラウザでは hydrate() を使用します。ルーティング、ヘッダー、リクエスト処理は自分で管理します。
ほとんどが静的コンテンツで、いくつかの動的ルートがあるルートごとに export const prerender = true(または 'auto' で人気ページをプリレンダリングし、ロングテールをサーバーレンダリング)を設定した SvelteKit。動的ルートはここでのすべてのルールに従います。

SSR バグを「修正」するために export const ssr = false を設定しないでください。それはルートをクライアントレンダリングのシェル(SPA)に変え、SEO と最初のペイントを失います。これはブラウザのグローバルなしでは本当にレンダリングできないページにのみ正当です。csr = false は逆のスイッチで、ハイドレーションなしのサーバー HTML を生成し、インタラクティビティが不要なページに使用します。

両方のパス、アダプター、ページオプションのセットアップ手順は references/ssr-build.md にあります。svelte.config.js / vite.config.js を生成する前にそれを読んでください。なぜなら、オプション名とアダプターの動作はバージョンによって異なり、微妙に間違えやすいからです。

ステップ2 — サーバー上で実行されるルーンを知る

この表がスキルの核心です:

ルーンSSR 中メモ
$state(v)実行される初期値がレンダリングされます。レンダリング中に値を変更しても、すでに出力された HTML は再レンダリングされません。SSR は単一パスです。
$state.raw(v)実行されるプロキシなし。変更ではなく完全に置き換える大きな data ペイロードに適しています。
$derived(expr)実行されるレンダリングの一部として評価されるため、その出力が HTML に含まれます。純粋でブラウザ専用APIがないようにしてください。
$effect / $effect.pre決して実行されないブラウザ専用。これが設定する状態は SSR HTML に存在しません(ハイドレーション後にポップイン)。これはハイドレーションエラーではありませんが、空の HTML と視覚的なレイアウトシフトを引き起こします。
$effect.root(fn)決して実行されない同上。その中でサーバーから見える状態を構築しないでください。
$props() / $bindable()実行されるロードされたデータは props として到着し、初期値がレンダリングされます。
$props.id()実行されるSSR セーフな一意の ID。サーバーとクライアント間で一貫しています(Svelte ≥ 5.20)。for/aria-labelledby リンクに使用します。
$inspect(v)開発のみSSR 出力なし。
$state.snapshot(v)実行される非 Svelte API やサーバーライブラリに状態を渡す前に使用します。
$effect.tracking()falseサーバーコンパイラはこれをリテラルの false に置き換えます。SSR 中はトラッキングコンテキストがありません。

この表が防ぐ古典的な SSR バグ:

<!-- ❌ サーバー HTML では空、ハイドレーション後にポップイン -->
<script>
  let user = $state();
  $effect(() => { user = getUser(); });
</script>
<div>{user?.name}</div>

修正はほとんどの場合同じです。SSR から見える値を $derived で計算するか、load でフェッチして props として渡します。$effect はブラウザにのみ存在するもの(キャンバス、オブザーバー、アナリティクス、ストレージ同期)のために残します。

完全なルーンごとのサーバー動作、ハイドレーションセーフな値のパターン、実験的な非同期 SSR のステータスについては references/runes-ssr.md を読んでください。

ステップ3 — 状態をリクエストごとに保ち、モジュールグローバルにしない

// ❌ サーバープロセス全体で1つの共有オブジェクト — ユーザーAがユーザーBのデータを見られる
// src/lib/user.svelte.ts
export const user = $state({ name: '' });

正規の修正方法は、load からリクエストごとのデータを返し、それを props として渡すことです。多くのネストされたコンポーネントが必要とする場合は、ゲッターとともにコンテキストに配置します:

<!-- +layout.svelte -->
<script>
  import { setContext } from 'svelte';
  let { data } = $props();
  setContext('user', () => data.user);
</script>
<!-- 任意の子コンポーネント -->
<script>
  import { getContext } from 'svelte';
  const user = getContext('user');
</script>
<p>こんにちは {user().name}</p>

コンテキストはコンポーネントツリーにアタッチされ、各 SSR リクエストは独自のツリーを持つため、ユーザー間でリークすることはありません。setContext には値そのものではなく、関数(またはゲッターを持つオブジェクト)を渡してください。そうしないとリアクティビティが失われます。多くのサーバー関数が必要とするリクエストごとのデータは、hooks.server.ts で設定された event.locals に格納できます。

モジュールレベルの状態は、ユーザーデータに触れない(テーマ、機能フラグなど)クライアント専用のアプリ状態には依然として問題ありません。ただし、そこからパーソナライズされた値をサーバーレンダリングしないことが条件です。

ステップ4 — ハイドレーションを契約として扱う

ハイドレーションはサーバー HTML を再生します。クライアントの最初のレンダリングはそれと一致しなければなりません。異なる値があると hydration_mismatch が発生します。これは意図的に抑制できないため、警告ではなくコードを修正してください。

  • 決定論的にレンダリングする:レンダリングパスで Date.now()、new Date() のフォーマット、Math.random()、crypto.randomUUID()、localStorage/window/navigator の読み取りを行わない。
  • ID は乱数やモジュールカウンターではなく $props.id() で生成する。
  • 日付は load 内でサーバー上でフォーマットし、文字列を渡すか、常に明示的な timeZone を渡す — サーバーとクライアントのタイムゾーンは異なります。
  • クライアント専用の値(ストレージ、メディアクエリ、位置情報):サーバーセーフなデフォルトから始め、$effect/onMount 内で埋めるか、安定したサーバーフォールバックを持つ mounted フラグでマークアップを制御する。
<script>
  import { browser } from '$app/environment';

  let theme = $state('light');
  $effect(() => {
    if (!browser) return;
    theme = localStorage.getItem('theme') ?? 'light'; // マウント後なのでハイドレーションが一致する
  });
</script>

プレーンな Svelte SSR(SvelteKit なし)では $app/environment は利用できません。レンダリングパスでは決して使わず、エフェクト内で typeof window !== 'undefined' を使用してください。

ステップ5 — サーバー上でデータを読み込み、ストリーミングする

  • +page.server.ts / +layout.server.ts はデータベース、ファイル、プライベート環境変数用。ユニバーサルな +page.ts はどちらでも実行できる公開 API 用。
  • サーバー load は devalue シリアライゼーションに耐えられるデータを返す必要があります:JSON に加えて Date、Map、Set、BigInt、RegExp、循環参照。関数やクラスインスタンスは不可。
  • 低速で必須でないデータ:プロミスを返し、ページ内で {#await} する — ネストされたプロミスは解決されるたびにストリーミングされます。必須データは最初のペイントに含めるために、返す前に await してください。
  • load 内で event.fetch を使用する:リクエストのクッキーを継承し、ハイドレーション時に再フェッチせずにレスポンスをインライン化します。
  • ナビゲーション後、data はコンポーネントを再作成せずに更新されます。非リアクティブなローカル変数にコピーするのではなく、$derived を使って派生させてください。

コンポーネント内の await やリモート関数を使用する非同期 SSR は、Svelte 5.57 / SvelteKit 2.7x(2026年9月時点)でもまだ実験的です。私が明示的に要求しない限り、本番コードで compilerOptions.experimental.async を有効にしないでください。正確なステータスとオプトインの方法は references/runes-ssr.md にあります。

ステップ6 — SSR セキュリティルールを適用する

  1. シークレットは秘密に保つ。 $env/static/private / $env/dynamic/private はサーバー専用です。PUBLIC_ プレフィックスが付いた変数だけがブラウザに到達します。$lib/server/ や *.server.js からのインポートは、コンポーネントがそれらを取り込もうとするとエラーになります。
  2. load から返されたものはすべて HTML ペイロードに埋め込まれます — レンダリングするフィールドだけでなく、すべてです。クライアントに不要なパスワードハッシュ、トークン、内部フィールドを決して返さないでください。
  3. CSP とノンス。 kit.csp を設定します。SvelteKit は自身のインラインスクリプトにノンスを注入し、%sveltekit.nonce% は app.html 内の手動タグをカバーします。実際のサーバーは実際の CSP ヘッダーを送信できるため、脆弱な <meta> タグの代わりにそれを使用してください。
  4. {@html} は依然として XSS です。 サーバーレンダリングされたからといってサニタイズされているわけではありません。信頼できない HTML はレンダリング直前に DOMPurify でサニタイズしてください。
  5. 認証は hooks.server.ts(locals を設定)とルートレベルのチェックに属します。 レイアウトの load 関数はすべてのクライアントサイドナビゲーションで実行されるわけではありません。
  6. CSRF 保護はフォーム送信に対してデフォルトで有効です。 そのままにしておき、カスタムの +server.js ミューテーションエンドポイントでは自分でオリジンをチェックしてください。
  7. セッションクッキー: httpOnly、本番では secure、最低でも sameSite: 'lax'。
  8. ユーザー指定の URL に対するサーバーサイドの fetch は SSRF です。 ホストを許可リストに登録し、プライベート IP とクラウドメタデータエンドポイントをブロックしてください。

完全なチェックリストとコード:references/security.md。

既存アプリのレビュー

構築ではなく監査やデバッグがタスクの場合は、references/ssr-review.md を参照してください:レンダリングモードのチェック、サーバーから見えないルーン、ハイドレーションの危険性、リクエストスコープの状態、ペイロードの露出、セキュリティグレップ、障害訓練。結果は ファイル:行 形式で、それぞれが引き起こす本番障害とともに報告してください。

参照ファイル

  • references/runes-ssr.md — ルーンごとのサーバー動作、ハイドレーションセーフなパターン、リクエストスコープの状態、プレーンな svelte/server の render + hydrate、実験的な非同期 SSR / リモート関数のステータス。
  • references/ssr-build.md — SvelteKit のセットアップとアダプター、ページオプション(ssr/csr/prerender)、フック、フォームアクション、ストリーミング、プレーンな Vite SSR、デプロイノート。
  • references/ssr-review.md — 既存の SSR アプリの監査チェックリスト:レンダリングモード、サーバーから見えないルーン、ハイドレーションの危険性、状態スコーピング、ペイロードの露出、セキュリティグレップ、障害訓練。
  • references/security.md — シークレットと環境変数、CSP ノンス、XSS、CSRF、認証/セッション処理、SSRF、エラー漏洩、出荷前チェックリスト。
  • assets/ — コピー可能なスターターファイル:svelte.config.js(CSP 付き SvelteKit)、hooks.server.ts(認証とセキュリティヘッダー)、プレーンな Vite SSR 用の index.html / entry-server.js / entry-client.js(この3つは CI で tests/plain-ssr によってビルドされます)。

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

関連スキル