Svelte 开发最佳实践
如果你已经掌握了 Svelte 的基础知识,并开始构建实际应用,你可能已经注意到官方文档主要介绍了各种功能”是什么”,但并不总是说明”何时”或”为什么”使用它们。本文聚焦于 Svelte 5 的最佳实践,旨在提升生产代码的可维护性、性能与清晰度,前提是你已经理解组件和响应式的基本工作原理。
核心要点
- 仅在值需要驱动 UI 更新时使用
$state;如果是替换值而非修改值,请使用$state.raw。 - 对于计算值,优先使用
$derived而非$effect;将$effect保留用于与外部系统同步。 - 在 SSR 环境中避免使用模块级状态。使用 Svelte 的 context API 配合基于类的
$state,以实现类型安全、按请求隔离的共享状态。 - 在 SvelteKit 中,使用
+page.server.js处理服务端页面数据,使用+server.js处理独立的 API 端点。 - 在新代码中采用现代 Svelte 5 语法(
onclick、{#snippet}、$props()),而非旧式写法。
Svelte 5 Runes:精确使用
Svelte 5 的 runes 是其核心响应式模型,正确使用它们比无处不用更重要。
只在变量需要驱动 UI 更新时使用 $state。 对于其他场景,普通变量更经济也更清晰。
当状态是一个会被替换而非修改的大型对象或数组时,请改用 $state.raw:
// ❌ 对仅会被重新赋值的 API 数据来说,代理开销没有必要
let users = $state(await fetchUsers());
// ✅ 当只是替换而非修改时,无需代理开销
let users = $state.raw(await fetchUsers());
当你需要直接修改嵌套属性(如 cart.items[0].quantity++)时使用 $state;当你是替换整个值时使用 $state.raw。
计算值优先使用 $derived 而非 $effect
这是现代 Svelte 开发中最常见的错误之一:
let num = $state(0);
// ❌ 避免 —— 制造了不必要的副作用
let square = $state(0);
$effect(() => { square = num * num; });
// ✅ 正确 —— 声明式且自动追踪依赖
let square = $derived(num * num);
$effect 是一种”逃生舱”。请将其保留用于与外部系统(如 D3)同步,对于自然契合的 DOM 层集成,可以考虑使用 {@attach}。
将 Props 视为动态值
从 props 派生的值应使用 $derived,而非普通赋值:
let { type } = $props();
// ✅ 当 `type` 变化时自动保持同步
let color = $derived(type === 'danger' ? 'red' : 'green');
Discover how at OpenReplay.com.
类型安全的 Context 优于共享模块
对于在组件子树中共享的状态,请优先使用 Svelte 的 context API,而非模块级状态。在 SSR 环境中,模块状态会跨请求持久化,可能导致数据在用户之间泄露。
现代的做法是使用带有 $state 字段的类:
// lib/theme.svelte.ts
import { getContext, setContext } from 'svelte';
class ThemeContext {
current = $state('light');
toggle() {
this.current = this.current === 'light' ? 'dark' : 'light';
}
}
const KEY = Symbol('theme');
export const setTheme = () => setContext(KEY, new ThemeContext());
export const getTheme = () => getContext<ThemeContext>(KEY);
这一种模式同时提供了类型安全、响应式状态以及合理的 SSR 作用域隔离。
SvelteKit 数据加载:选择合适的模式
SvelteKit 中一个常见的困惑点是:何时使用 +page.server.js,何时使用 +server.js?
| 场景 | 使用 |
|---|---|
| 为带有 SSR 或仅服务端访问的页面获取数据 | +page.server.js 配合 load() |
| 构建供外部使用的 API 端点 | +server.js |
| 客户端水合后才需要的数据 | onMount + fetch |
对于需要服务端访问、密钥或 SSR 的页面数据,+page.server.js 通常是合适的默认选择。它在服务端运行,避免密钥暴露到客户端,并能与 SvelteKit 的 form actions 无缝集成,实现渐进增强。
一些实用的小技巧
带 key 的 {#each} 块可以防止微妙的 DOM 复用 bug。请始终使用稳定且唯一的 ID 作为 key,切勿使用索引。
$inspect.trace 在调试响应式问题时被低估了。在任何 $effect 或 $derived.by 的顶部加上它,就能精确看到是哪个依赖触发了重新执行。
优先使用 Snippets 而非 Slots 来复用标记片段。Snippets 组合性更好,并且可以作为 props 传递,使组件 API 更清晰。
在新代码中避免旧式语法。 用 onclick 替换 on:click,用 {#snippet} 替换 <slot>,用 $props() 替换 export let。这些模式与现代 Svelte 5 的约定和当前编译器行为保持一致。
结语
Svelte 5 奖励克制。响应式作用域越精确——仅在需要时使用 $state、用 $derived 替代 $effect、用 context 替代模块全局变量——你的应用就越可预测且性能越好。先从能解决问题的最简单响应式原语开始,只有当简单工具确实不够用时,才转向更强大的工具。
常见问题
当你打算整体替换某个值而非局部修改时使用 $state.raw,例如存储获取到的 API 响应或会被整体重新赋值的大数组。它跳过了响应式代理的开销,对大数据集而言能提升性能。当你需要对嵌套修改进行细粒度响应(如更新数组中的某项)时,请使用普通的 $state。
$derived 是声明式的,会自动追踪依赖,并产生一个保持同步的只读值。$effect 以副作用形式命令式地执行,更难推理,可能引入时序 bug 和不必要的重复执行。请将 $effect 保留用于与 Svelte 响应式之外的系统同步,例如第三方库、canvas API 或手动 DOM 操作。
在 SvelteKit 等 SSR 环境中并不安全。模块级状态会被服务器处理的所有请求共享,可能导致一个用户的数据泄露到另一个用户的会话中。请使用 Svelte 的 context API,配合 setContext 和 getContext,最好以包含 $state 字段的类作为载体。这样能在保持响应式和类型安全的同时,将状态作用域限定在每个请求和组件树中。
当数据属于特定页面,并且需要 SSR、SEO、服务端访问或表单操作时,使用 +page.server.js。当你需要一个独立的 HTTP 端点时使用 +server.js,例如供外部客户端使用的 JSON API、webhook 或非页面的 fetch 调用。如果数据仅由某个页面渲染且需要服务端能力,+page.server.js 通常是更好的默认选择。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.