Svelte 5 服务端渲染(Runes) 是做什么的?
本技能涵盖内容
“SSR” 意味着每个请求都由服务器(Node、Bun、Serverless 或 Edge)渲染为 HTML,然后浏览器接收;同样的组件随后水合为交互式应用。与纯静态构建不同,请求时存在真实的服务器,因此允许使用 +page.server.js、+layout.server.js、+server.js、表单操作和 hooks.server.js——这些正是数据和机密信息所属的位置。
Runes 部分是 SSR 变得棘手的原因:每个组件运行两次,一次在服务器上,一次在浏览器中。在纯客户端应用中正常的代码在这里会出问题:
$effect永远不会在服务器上运行,因此其填充的任何状态都会在服务器 HTML 中缺失。.svelte.ts文件中的模块级$state在每个服务器进程中只创建一次,并在所有用户的请求间共享。- 在渲染期间根据
Date.now()、Math.random()或localStorage计算的值在服务器和客户端之间不同,导致水合不匹配。
这三种失败占了 Runes 代码中大多数 SSR 错误,每种下面都有一种简单的正确模式。
第一步——选择项目形态
| 需求 | 使用 |
|---|---|
| 完整应用:路由、服务端数据加载、表单、认证、流式传输 | SvelteKit,配合 adapter-auto(自动检测 Vercel/Netlify/Cloudflare)、adapter-node(自托管)或平台适配器。这是默认选项;除非有理由不这么做,否则使用它。 |
| 在现有后端(Express/Hono/Fastify/Bun)内部的 Svelte 组件树 | 纯 Vite + Svelte 5:服务器端使用 svelte/server 的 render(),浏览器端使用 hydrate()。你自行控制路由、头部和请求处理。 |
| 以静态内容为主,少数动态路由 | SvelteKit,每路由设置 export const prerender = true(或使用 'auto' 预渲染热门页面,对长尾请求进行服务端渲染)。动态路由仍然遵循此处的每一条规则。 |
不要设置 export const ssr = false 来“修复”SSR 错误——那会将路由变成客户端渲染的壳(SPA),失去 SEO 和首次绘制。只有当页面确实无法在没有浏览器全局变量的情况下渲染时,这个设置才合理。csr = false 是相反的开关:服务端 HTML 无水合,适用于不需要交互性的页面。
两种路径的设置步骤,以及适配器和页面选项,都在 references/ssr-build.md 中。请在生成 svelte.config.js / vite.config.js 之前阅读它,因为选项名称和适配器行为在不同版本之间存在差异,并且容易细微出错。
第二步——了解哪些 Runes 在服务器上运行
此表是技能的核心:
| Rune | 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>
修复方法几乎总是相同:在 $derived 中计算 SSR 可见的值,或者在 load 中获取它们并作为 props 传递。将 $effect 保留给仅存在于浏览器中的东西(canvas、观察者、分析工具、存储同步)。
阅读 references/runes-ssr.md 获取完整的逐 Rune 服务端行为、水合安全值模式以及实验性异步 SSR 状态。
第三步——保持状态每个请求独立,避免模块全局
// ❌ 为整个服务器进程共享一个对象——用户 A 可以看到用户 B 的数据
// src/lib/user.svelte.ts
export const user = $state({ name: '' });
规范的修复方法是从 load 返回每个请求的数据并作为 props 向下传递;当许多嵌套组件需要它时,使用 Getter 将其放入上下文:
<!-- +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 请求都有自己的树,因此它不会在用户之间泄漏。将函数(或带有 getter 的对象)传入 setContext,而不是值本身,否则会丢失响应性。许多服务器函数需要的每个请求数据可以放在 event.locals 上,在 hooks.server.ts 中填充。
模块级状态对于不涉及任何用户数据的客户端专用应用状态(主题、功能标志)仍然是安全的,只要没有内容从它渲染个性化值。
第四步——将水合视为契约
水合会重放服务器 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'; // 挂载后,以便水合匹配
});
</script>
在纯 Svelte SSR(无 SvelteKit)中,$app/environment 不可用——在 effect 内部使用 typeof window !== 'undefined',绝不在渲染路径中使用。
第五步——在服务端加载和流式传输数据
+page.server.ts/+layout.server.ts用于数据库、文件和私有环境变量;通用+page.ts用于可以在任一位置运行的公共 API。- 服务端
load必须返回能够通过 devalue 序列化的数据:JSON 加上Date、Map、Set、BigInt、RegExp和循环引用。不能有函数或类实例。 - 慢速、非必要数据:返回 Promise 并在页面中使用
{#await}——嵌套的 Promise 会在解析时流式传输。在返回之前等待必要数据,以便其出现在首次绘制中。 - 在
load内部使用event.fetch:它继承请求的 Cookies,并内联响应以供后续水合,而不是重新请求。 - 导航后,
data更新而不会重新创建组件:从它派生($derived),而不是将其复制到非响应式局部变量中。
截至 Svelte 5.57 / SvelteKit 2.7x(2026 年 9 月),组件中的异步 SSR(await)和远程函数仍处于实验阶段。除非我明确要求,否则不要在生产代码中启用 compilerOptions.experimental.async;确切的状态和启用方式在 references/runes-ssr.md 中。
第六步——应用 SSR 安全规则
- 机密信息保密。
$env/static/private/$env/dynamic/private仅在服务器端;只有PUBLIC_前缀的变量会到达浏览器。从$lib/server/和*.server.js导入会在组件引入时抛出异常。 - 从
load返回的任何内容都会嵌入到 HTML 有效载荷中——全部内容,而不仅仅是你渲染的字段。绝不要返回不需要在客户端上的密码哈希、令牌或内部字段。 - 带有 nonce 的 CSP。 配置
kit.csp;SvelteKit 会向自己的内联脚本注入一个 nonce,%sveltekit.nonce%覆盖app.html中的手动标签。真实的服务器可以发送真实的 CSP 头部——使用它而不是较弱的<meta>标签。 {@html}仍然是 XSS。 服务端渲染并不意味着已清理。在渲染前立即使用 DOMPurify 清理不可信的 HTML。- 认证属于
hooks.server.ts(填充locals)加上路由级检查。布局load函数不会在每次客户端导航时运行。 - 表单提交默认启用 CSRF 保护;保持启用,并在任何自定义的
+server.js变更端点中自行检查来源。 - 会话 Cookies:
httpOnly、生产环境secure、sameSite: 'lax'至少。 - 服务端对用户提供的 URL 执行
fetch是 SSRF。 对主机进行白名单处理;阻止私有 IP 和云元数据端点。
完整清单和代码:references/security.md。
审查现有应用
当任务是审计或调试而不是构建时,请参考 references/ssr-review.md:渲染模式检查、服务器不可见的 Runes、水合风险、请求作用域状态、有效载荷暴露、安全检查和故障演练。按 文件:行号 报告发现的问题,并附带每个问题会导致的生产故障。
参考文件
references/runes-ssr.md——逐 Rune 服务端行为、水合安全模式、请求作用域状态、纯svelte/serverrender +hydrate,以及实验性异步 SSR / 远程函数状态。references/ssr-build.md——SvelteKit 设置和适配器、页面选项(ssr/csr/prerender)、hooks、表单操作、流式传输、纯 Vite SSR 和部署说明。references/ssr-review.md——现有 SSR 应用的审计清单:渲染模式、服务器不可见的 Runes、水合风险、状态作用域、有效载荷暴露、安全检查和故障演练。references/security.md——机密信息和环境变量、CSP nonces、XSS、CSRF、认证/会话处理、SSRF、错误泄漏和发布前清单。assets/——可复制的入门文件:svelte.config.js(带 CSP 的 SvelteKit)、hooks.server.ts(认证加安全头部),以及用于纯 Vite SSR 的index.html/entry-server.js/entry-client.js(这三件套由tests/plain-ssr在 CI 中构建)。