Communitygithub.com

Svelte 5 伺服器端渲染 (Runes)

使用 runes($state、$derived、$effect、$props、$bindable)搭配 SvelteKit SSR 或純 Vite SSR 來建構、遷移或審查伺服器端渲染的 Svelte 5 應用程式。

Svelte 5 伺服器端渲染 (Runes) 是什麼?

這項技能提供了一個全面的指南,用於使用 Svelte 5 的 runes API 進行伺服器端渲染 (SSR)。它涵蓋了兩種主要的專案架構:完整的 SvelteKit 應用程式(配備 adapter-auto、adapter-node 或各平台轉接器),以及建立在 Express/Hono/Fastify/Bun 等現有後端之上的純 Vite SSR 設定。該指南強調了 SSR 特有的關鍵挑戰,最著名的是「雙重執行」問題,即每個元件在伺服器和瀏覽器上都運行一次,這導致了常見的錯誤,例如 `$effect` 程式碼在伺服器上從不執行、模組級別的 `$state` 在使用者之間洩漏,以及由 `Date.now()` 或 `Math.random()` 等非確定性值引起的 hydration 不匹配。它提供了一個詳細的表格,說明了每個 rune 在 SSR 期間的行為,並提供了針對每個問題的具體修復模式,例如使用 `$derived` 來計算伺服器可見的值,或透過 Svelte 的 context API 來確保狀態是按請求範圍隔離的。此外,該技能還涵蓋了安全的伺服器資料載入(使用 `load` 函數)、用於改善感知效能的串流傳輸、用於確保伺服器和用戶端之間一致性的 hydration 合約,以及一套全面的 SSR 安全規則,用於保護秘密、強制執行 CSP 及防止 XSS、CSRF 和 SSRF 攻擊。它還包括用於審查現有 SSR 應用程式的參考資料,提供檢查渲染模式、hydration 風險、狀態範圍、payload 暴露和安全性弱點的清單。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agentmemoir/skills/tree/HEAD/skills/frontend/svelte5-ssr-runes

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Svelte 5 伺服器端渲染 (Runes) 是做什麼的?

此技能涵蓋的範圍

「SSR」意指每個請求都由伺服器(Node、Bun、無伺服器或邊緣環境)渲染成 HTML,然後瀏覽器才接收到該 HTML;之後同樣的元件會進行 hydration 變成一個互動式應用程式。與純靜態建置不同,在請求時是存在真實伺服器的,因此可以使用 +page.server.js、+layout.server.js、+server.js、表單動作(form actions)和 hooks.server.js——而這些正是資料和機密所屬的地方。

runes 的部分正是讓 SSR 變得棘手的原因:每個元件都會執行兩次,一次在伺服器上,一次在瀏覽器中。在純客戶端應用程式中沒問題的程式碼,在這裡就會出錯:

  • $effect 在伺服器上永遠不會執行,因此它填充的任何狀態都會從伺服器 HTML 中遺失。
  • .svelte.ts 檔案中的模組層級 $state 只為每個伺服器程序建立一次,並在所有使用者的請求之間共享。
  • 在渲染期間從 Date.now()、Math.random() 或 localStorage 計算出的值,在伺服器和用戶端之間會有所不同,導致 hydration 不匹配。

這三個失敗構成了 runes 程式碼中大部分的 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 的 SvelteKit(或使用 'auto' 來預渲染熱門頁面,並對長尾頁面進行伺服器渲染)。動態路由仍然遵循這裡的所有規則。

請勿設定 export const ssr = false 來「修復」SSR 錯誤——這會將路由變成一個僅客戶端渲染的 shell(一個 SPA),從而失去 SEO 和首次渲染效能。它僅適用於那些確實無法在沒有瀏覽器全域變數的情況下渲染的頁面。csr = false 則是相反的開關:產生無 hydration 的伺服器 HTML,適用於不需要互動性的頁面。

這兩種路徑的設定步驟,以及轉接器和頁面選項,都在 references/ssr-build.md 中。在產出 svelte.config.js / vite.config.js 之前請先閱讀它,因為選項名稱和轉接器行為在不同版本之間有所差異,且容易犯下難以察覺的錯誤。

步驟 2 — 了解哪些 runes 會在伺服器上執行

下表是此技能的核心:

Rune在 SSR 期間備註
$state(v)執行初始值會被渲染。在渲染過程中改變它不會重新渲染已發出的 HTML——SSR 是單次傳遞。
$state.raw(v)執行無代理(proxy)。適用於你會整體替換而非逐步修改的大量 data payload。
$derived(expr)執行作為渲染的一部分被評估,因此其輸出會進入 HTML。保持其純淨,且不含僅瀏覽器適用的 API。
$effect / $effect.pre永不執行僅限瀏覽器。它所設定的狀態會從 SSR HTML 中遺失(hydration 後才出現)。這不是 hydration 錯誤,但會造成空的 HTML 和明顯的版面位移(layout shift)。
$effect.root(fn)永不執行同上。請勿在其中建構伺服器可見的狀態。
$props() / $bindable()執行載入(load)資料會以 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 中為空,然後在 hydration 後突然出現 -->
<script>
  let user = $state();
  $effect(() => { user = getUser(); });
</script>
<div>{user?.name}</div>

修復方法幾乎總是相同的:在 $derived 中計算 SSR 可見的值,或者在 load 中擷取它們並以 props 形式傳遞。將 $effect 保留給僅存在於瀏覽器中的事物(canvas、觀察器、分析、儲存同步)。

請閱讀 references/runes-ssr.md 以獲取完整的逐個 rune 伺服器行為、hydration 安全值的模式,以及實驗性的非同步 SSR 狀態。

步驟 3 — 保持狀態按請求隔離,絕不使用模組全域變數

// ❌ 一個共享物件用於整個伺服器程序——使用者 A 可以看到使用者 B 的資料
// src/lib/user.svelte.ts
export const user = $state({ name: '' });

標準的修復方法是從 load 返回按請求隔離的資料,並以 props 形式傳遞下去;當許多巢狀元件都需要它時,請搭配 getter 將其放入 context 中:

<!-- +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>Hi {user().name}</p>

Context 依附於元件樹,而每個 SSR 請求都有自己的元件樹,因此它不會在使用者之間洩漏。將一個函數(或帶有 getter 的物件)傳入 setContext,而不是值本身,否則會失去反應性。許多伺服器函數需要的按請求資料,可以存放在 event.locals 上,並在 hooks.server.ts 中填入。

對於不接觸任何使用者資料的純客戶端應用程式狀態(主題、功能標記),模組層級的狀態仍然可以安全使用,只要伺服器不要從中渲染出個人化的值即可。

步驟 4 — 將 Hydration 視為一份合約

Hydration 會重播伺服器的 HTML;用戶端的首次渲染必須與之匹配。任何不同的值都會導致 hydration_mismatch,這被故意設計為不可抑制的——修正程式碼,而不是忽略警告。

  • 確定性地渲染:不要在渲染路徑中使用 Date.now()、new Date() 格式化、Math.random()、crypto.randomUUID()、localStorage / window / navigator 讀取。
  • 使用 $props.id() 產生 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'; // 在 mount 之後,因此 hydration 可以匹配
  });
</script>

在純 Svelte SSR(無 SvelteKit)中,$app/environment 不可用——請在 effect 內部使用 typeof window !== 'undefined',永遠不要在渲染路徑中使用。

步驟 5 — 在伺服器上載入和串流傳輸資料

  • 使用 +page.server.ts / +layout.server.ts 來處理資料庫、檔案和私有環境變數;使用通用 +page.ts 來處理可在任一位置運行的公開 API。
  • 伺服端的 load 必須返回能夠經得起 devalue 序列化的資料:包括 JSON 加上 Date、Map、Set、BigInt、RegExp 以及循環引用。不包含函數或類別實例。
  • 慢速、非必要的資料:返回 promise 並在頁面中使用 {#await}——巢狀的 promise 會在其解析時進行串流傳輸。在返回之前等待必要的資料,以便它們能出現在首次渲染中。
  • 在 load 內部使用 event.fetch:它會繼承請求的 cookie,並將回應內聯以進行 hydration,而不是重新擷取。
  • 導航後,data 會在不重建元件的情況下更新:從它推導出($derived)值,而不是將其複製到非反應性的區域變數中。

在 Svelte 5.57 / SvelteKit 2.7x(2026 年 9 月)之前,非同步 SSR(在元件中使用 await)和遠端函數仍然是實驗性的。除非我明確要求,否則不要在生產程式碼中啟用 compilerOptions.experimental.async;確切狀態和選擇加入方式在 references/runes-ssr.md 中。

步驟 6 — 應用 SSR 安全規則

  1. 機密保持私有。 $env/static/private / $env/dynamic/private 僅限伺服器使用;只有 PUBLIC_ 前綴的變數才能到達瀏覽器。從 $lib/server/ 和 *.server.js 的匯入,如果元件拉入它們,則會拋出錯誤。
  2. 從 load 返回的任何東西都會嵌入到 HTML payload 中——所有內容,而不僅僅是你渲染的欄位。絕對不要返回你不需要在客戶端使用的密碼哈希、令牌或內部欄位。
  3. 使用 nonce 的 CSP。 設定 kit.csp;SvelteKit 會將其 nonce 注入到自己的內聯腳本中,而 %sveltekit.nonce% 涵蓋了 app.html 中的手動標籤。真實的伺服器可以發送真實的 CSP 標頭——請使用這個,而不是較弱的 <meta> 標籤。
  4. {@html} 仍然是 XSS。 伺服器渲染不代表已清理。在渲染前立即使用 DOMPurify 清理不受信任的 HTML。
  5. 認證屬於 hooks.server.ts(填入 locals)以及路由層級的檢查。佈局(Layout)的 load 函數不會在每次客戶端導航時都執行。
  6. 表單提交預設開啟 CSRF 保護;請保持開啟,並在你的任何自訂 +server.js 變動端點中自行檢查來源。
  7. Session Cookie: 應設定為 httpOnly、在生產環境中為 secure,且 sameSite: 'lax' 為最低要求。
  8. 伺服器端對使用者提供的 URL 進行 fetch 是 SSRF。 請對主機進行白名單列管;封鎖私有 IP 和雲端中繼資料端點。

完整檢查清單和程式碼:references/security.md。

審查現有應用程式

當任務是稽核或除錯而不是建置時,請依照 references/ssr-review.md 進行操作:渲染模式檢查、伺服器不可見的 runes、hydration 危險、按請求的作用域狀態、payload 暴露、安全性搜尋和故障演練。以「檔案:行號」以及每個問題會導致的生產環境失敗來報告發現。

參考檔案

  • references/runes-ssr.md — 逐個 rune 的伺服器行為、hydration 安全模式、按請求的作用域狀態、純 svelte/server render + hydrate,以及實驗性的非同步 SSR / 遠端函數狀態。
  • references/ssr-build.md — SvelteKit 設定和轉接器、頁面選項 (ssr/csr/prerender)、hooks、表單動作、串流、純 Vite SSR 以及部署注意事項。
  • references/ssr-review.md — 現有 SSR 應用程式的稽核查檢清單:渲染模式、伺服器不可見的 runes、hydration 危險、狀態作用域、payload 暴露、安全性搜尋和故障演練。
  • references/security.md — 機密和環境變數、CSP nonce、XSS、CSRF、認證/Session 處理、SSRF、錯誤洩漏以及上線前檢查清單。
  • assets/ — 可複製的入門檔案:svelte.config.js(帶有 CSP 的 SvelteKit)、hooks.server.ts(帶有安全標頭的認證),以及用於純 Vite SSR 的 index.html / entry-server.js / entry-client.js(這個三重奏由 tests/plain-ssr 在 CI 中建置)。

Individual skills in this repo

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

相關技能