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