Nuxt vs SvelteKit: What works better?

Nuxt vs SvelteKit: What works better?

Nuxt vs SvelteKit. Which one is better? That is is what I've been testing out this week. I built the same app twice. Once in Nuxt with the version 5 compatibility preview turned on, and once in SvelteKit using its experimental remote functions. I created a basic task app. Where you can add or remove tasks. I also logged the network requests and timing. The biggest thing I noticed was that the SvelteKit app would make one request, when creating a new task, while the Nuxt app needed two. That one difference turned out to be the most interesting part of the whole comparison, so let's jump in. The setup Both apps are a simple task list backed by an in browser memory store. Both are production builds running locally, and both render the initial task list on the server. Quick caveat, Nuxt 5 is not released yet. My Nuxt app is stable Nuxt 4.5 with the compatibility flag set: // nuxt.config.ts export default defineNuxtConfig({ compatibilityDate: '2026-07-01', future: { compatibilityVersion: 5, }, }) Enter fullscreen mode Exit fullscreen mode And SvelteKit's remote functions are still marked experimental in the docs. So this is a comparison of directions, not finished products. The numbers When I add a task in the Nuxt app, I get a POST to /api/tasks (about 690 ms) followed by a GET to /api/tasks (about 450 ms) to refresh the list. A little over 1,100 ms total, and the timeline panel reports two browser requests. When I add a task in the SvelteKit app, I get one request. About 1,100 ms. The server still does both operations, the mutation and the read, but they come back in a single response. Plain refreshes were nearly identical. 447 ms in Nuxt, 448 ms in SvelteKit. I ran this quite a few times, and if I had to pick, SvelteKit felt slightly faster overall. But the totals were close enough that I wouldn't choose a framework based on them. Let's talk about how requests work in each. The Nuxt version: explicit API routes If you've used Nuxt before, this will feel familiar. I have two handlers in 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 The POST handler validates the title and saves the task. These are normal HTTP endpoints. Anything that speaks HTTP can call them. On the page, useFetch loads the initial data during SSR, so hydration doesn't fetch it again. When I add a task, I post with $fetch and then call refresh(): const { data: snapshot, refresh } = await useFetch('/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 The code is explicit, and the network tab matches the code exactly. POST first, GET second. Could I avoid the second request? Sure. The POST could return the updated list and I could patch local state myself. I wrote it this way because invalidate-and-refetch is the workflow most of us reach for, and it's exactly the pattern SvelteKit's remote functions are designed to improve. I also find by adding a GET request we are verifying the exact output after the POST. The SvelteKit version: remote functions This is the experimental feature. You turn it on in svelte.config.js: const config = { kit: { adapter: adapter(), experimental: { remoteFunctions: true, }, }, compilerOptions: { experimental: { async: true, }, }, } Enter fullscreen mode Exit fullscreen mode Then you create a file ending in .remote.ts and export your server functions: // 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 The function bodies always run on the server. In the browser, they become typed wrappers around endpoints SvelteKit generates for you. I really enjoy how there is no public endpoint to hit, it's isolated. It's created for the call, which means I can use environment variables and secrets in there without thinking about it. The void getTasks().refresh() line is the API on the server side to trigger the refresh. After the mutation, SvelteKit refreshes the query on the server and packages the new value into the command's response. That's the single-flight mutation, and it's why the network tab shows one request instead of two. On the page, I just import and call the functions: import { addTask, getTasks } from './tasks.remote' const tasks = getTasks() async function submitTask(event: SubmitEvent) { event.preventDefault() await addTask({ requestId: crypto.randomUUID(), title }) } {@render dashboard(await tasks)} Enter fullscreen mode Exit fullscreen mode That await tasks at the bottom works almost like a subscription. When the server-side refresh happens, the task list updates automatically. No server routes to worry about. My first look at this pattern, I thought it was a little complicated. But it clicked pretty fast, and the query-refreshes-inside-the-command idea makes sense once you see it in the network tab. If you've used server actions in Next or TanStack Start, this will feel like family. I'd say I still like TanStack Start's server actions a bit better, but this is close. Heads up: remote functions have been available since SvelteKit 2.27 and they are still experimental. The API has changed several times over the past few months. If you adopt them early, pin your versions and budget time for migrations. What does Nuxt 5 offer? Nuxt 5 doesn't have an answer to remote functions. Most of the confirmed work is in the underlying structure. A new version of Nitro, a new Vite integration, and framework internals. The upgrade guide walks through what to expect, and it's mostly foundation work rather than new application-level APIs. My verdict I'm sticking with Nuxt. I love the API routes pattern, I love Vue, and nothing in this demo is a reason to rewrite an existing app. You can return updated data from a mutation today and skip the second request yourself if it matters. But I do miss server actions. Frameworks like SvelteKit, Next, and TanStack Start all have some version of a typed, non-public server function you can call from a component. I hope Nuxt adds something like it in a future update, beyond the server components they have now. If you're starting a SvelteKit project and can live with an experimental API, try remote functions first. The single-flight mutation is awesome, and the types crossing the boundary for free is a great developer experience. Which side are you on: explicit API routes, or remote functions that generate the transport for you? Let me know in the comments if you agree or disagree. BTW, I used Kiro for all my research for this post and video! Check it out , it's an amazing harness!

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.