Nuxt vs SvelteKit。哪个更好?

这就是我本周一直在测试的内容。我构建了同一个应用两次。一次使用 Nuxt 并开启版本 5 兼容性预览,另一次使用 SvelteKit 的实验性远程函数。

我创建了一个简单的任务应用。可以添加或删除任务。我还记录了网络请求和耗时。我注意到的最大区别是,创建新任务时,SvelteKit 应用只会发出一个请求,而 Nuxt 应用需要两个请求。

这一差异成为整个对比中最有趣的部分,我们来详细探讨。

设置

两个应用都是简单的任务列表,基于浏览器内存存储。都是在本地运行的生产构建版本,并且都在服务器上渲染初始任务列表。

需要说明的是,Nuxt 5 尚未正式发布。我的 Nuxt 应用是稳定的 Nuxt 4.5,并设置了兼容性标志:

// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2026-07-01',
  future: {
    compatibilityVersion: 5,
  },
})

Enter fullscreen mode Exit fullscreen mode

而 SvelteKit 的远程函数在文档中仍被标记为实验性功能。因此这是一个方向性的对比,而非成品对比。

数据对比

在 Nuxt 应用中添加任务时,我会看到一个 POST 请求到 /api/tasks(约 690 毫秒),随后是一个 GET 请求到 /api/tasks(约 450 毫秒)以刷新列表。总计略超过 1,100 毫秒,时间线面板显示两次浏览器请求。

Nuxt request

在 SvelteKit 应用中添加任务时,我只看到一个请求,耗时约 1,100 毫秒。服务器仍然执行了两个操作(写入和读取),但它们在单个响应中返回。

Svelte Request

普通刷新几乎相同。Nuxt 为 447 毫秒,SvelteKit 为 448 毫秒。我运行了多次,如果要选择,SvelteKit 总体感觉稍快一些。但总时间非常接近,我不会基于这个选择框架。

让我们来谈谈两者请求的工作方式。

Nuxt 版本:显式 API 路由

如果你用过 Nuxt,这会感觉很熟悉。我在 server/api 中有两个处理器:

// server/api/tasks.get.ts
import { defineEventHandler } from 'h3'
import { readTasks } from '../utils/task-store'

export default defineEventHandler(() => readTasks())

Enter fullscreen mode Exit fullscreen mode

POST 处理器验证标题并保存任务。这些是普通的 HTTP 端点。任何支持 HTTP 的客户端都可以调用它们。

在页面中,useFetch 在 SSR 期间加载初始数据,因此水合(hydration)不会再次获取。当我添加任务时,使用 $fetch 发出 POST 请求,然后调用 refresh()

const { data: snapshot, refresh } = await useFetch<TaskSnapshot>('/api/tasks', {
  key: 'task-dashboard',
})

async function submitTask() {
  await $fetch('/api/tasks', {
    method: 'POST',
    body: { requestId: crypto.randomUUID(), title },
  })
  await refresh()
}

Enter fullscreen mode Exit fullscreen mode

代码很明确,网络标签页与代码完全匹配:先 POST,再 GET。

能否避免第二次请求?当然可以。POST 可以返回更新后的列表,我可以自己修补本地状态。我这样写是因为 invalidate-and-refetch 是大多数人会采用的工作流程,而这正是 SvelteKit 远程函数旨在改进的模式。我还发现,通过添加 GET 请求,我们可以验证 POST 后的确切输出。

SvelteKit 版本:远程函数

这是实验性功能。你需要在 svelte.config.js 中开启:

const config = {
  kit: {
    adapter: adapter(),
    experimental: {
      remoteFunctions: true,
    },
  },
  compilerOptions: {
    experimental: {
      async: true,
    },
  },
}

Enter fullscreen mode Exit fullscreen mode

然后创建一个以 .remote.ts 结尾的文件并导出你的服务器函数:

// tasks.remote.ts
import { command, query } from '$app/server'
import { addTask as addTaskToStore, readTasks } from '$lib/server/task-store'
import * as v from 'valibot'

const taskInput = v.object({
  requestId: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(100)),
  title: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(80)),
})

export const getTasks = query(async () => readTasks())

export const addTask = command(taskInput, async ({ requestId, title }) => {
  const result = await addTaskToStore(title, requestId)

  // This runs on the server, and the refreshed query value
  // comes back in the same command response.
  void getTasks().refresh()

  return result
})

Enter fullscreen mode Exit fullscreen mode

函数体始终在服务器上运行。在浏览器中,它们会变成 SvelteKit 为你生成的端点的类型化包装器。我非常喜欢没有公开端点可供调用,它是隔离的。它是为调用创建的,这意味着我可以在其中使用环境变量和密钥而无需多想。

void getTasks().refresh() 这一行是服务器端触发刷新的 API。变更操作后,SvelteKit 会在服务器上刷新查询,并将新值打包到命令的响应中。这就是单次往返变更(single-flight mutation),也是网络标签页只显示一个请求而非两个的原因。

在页面中,我只需导入并调用这些函数:

<script lang="ts">
  import { addTask, getTasks } from './tasks.remote'

  const tasks = getTasks()

  async function submitTask(event: SubmitEvent) {
    event.preventDefault()
    await addTask({ requestId: crypto.randomUUID(), title })
  }
</script>

{@render dashboard(await tasks)}

Enter fullscreen mode Exit fullscreen mode

底部的 await tasks 几乎像一个订阅。当服务器端刷新发生时,任务列表会自动更新。无需担心服务器路由。

remotefunctions

我第一次看到这种模式时,觉得有点复杂。但很快我就理解了,一旦你在网络标签页中看到它,命令内部的查询刷新理念就说得通了。如果你用过 Next 或 TanStack Start 中的服务器操作(server actions),这会感觉很熟悉。我会说我还是更喜欢 TanStack Start 的服务器操作,但这个已经很接近了。

注意:远程函数自 SvelteKit 2.27 起可用,但仍处于实验阶段。API 在过去几个月里已经多次变更。如果你提前采用,请锁定版本并预留迁移时间。

Nuxt 5 提供了什么?

Nuxt 5 还没有对应远程函数的方案。大部分已确认的工作都在底层结构中。新版本的 Nitro、新的 Vite 集成,以及框架内部改进。升级指南介绍了预期内容,主要都是基础工作,而非新的应用级 API。

我的结论

我将继续使用 Nuxt。我喜欢 API 路由模式,喜欢 Vue,而这个演示中没有任何理由让我重写现有应用。如果你觉得有必要,今天就可以让变更操作返回更新后的数据,自己跳过第二次请求。

但我确实怀念服务器操作。SvelteKit、Next 和 TanStack Start 等框架都提供了某种版本的类型化、非公开服务器函数,可以从组件中调用。我希望 Nuxt 在未来的更新中添加类似功能,超越现有的服务器组件。

如果你正在启动一个 SvelteKit 项目,并且可以接受实验性 API,建议先尝试远程函数。单次往返变更非常棒,而且类型可以免费跨越边界,这是一种很棒的开发体验。

你站在哪一边:显式 API 路由,还是由框架为你生成传输层的远程函数?欢迎在评论中告诉我你同意还是不同意。

另外,我使用 Kiro 完成了这篇文章和视频的所有研究!快去看看吧,它是一个很棒的工具!