Next.js 14 introduced Server Actions as a stable feature, promising to eliminate the need for manually creating API endpoints for form mutations. But is it actually faster? I spent the weekend collecting data.
The Experiment Setup
I built two identical “Todo List” applications:
- App A: Uses standard
/api/todosroutes withGETandPOSThandlers. - App B: Uses Server Actions directly inside the component.
Both apps were deployed to Vercel (Edge Network) and connected to a Supabase Postgres instance in us-east-1.
Round 1: Code Volume & Complexity
API Routes: The API route approach required creating a separate file for the handler, defining TypeScript types for the request/response, and using fetch or a library like TanStack Query on the client.
Server Actions: I defined a function createTodo in a file marked “use server”. I called it directly from my form’s action prop. The type safety was automatic. No fetch, no serialization logic.
Winner: Server Actions. My production code reduced by about 40%.
Round 2: Latency (The Surprise)
I ran 100 sequential requests for each method. Here are the average round-trip times:
- API Route POST: 120ms
- Server Action: 145ms
Wait, what? Why are Server Actions slower?
Upon inspecting the network tab, I realized that Server Actions in Next.js perform a sequential re-validation of the page data by default. When you submit a form, Next.js not only executes the action but often re-renders the RSC payload for the current route to ensure the UI is up-to-date.
The “Optimistic” Factor
While raw latency was slightly higher, the perceived performance of Server Actions was vastly superior due to useOptimistic.
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
With this hook, the UI updates instantly (0ms). The network request happens in the background. Implementing this with standard API routes usually requires complex cache manipulation in tools like React Query.
Security Implications
This is where I see most juniors mess up. Just because a function is imported into a client component doesn’t mean it’s safe.
The Trap:
// server-action.ts
"use server"
export async function deleteUser(id: string) {
// ❌ MISSING AUTH CHECK
await db.delete(id);
}
Server Actions are public API endpoints under the hood. You MUST validate authentication and authorization inside every single action, just like you would for an API route.
My Verdict
I am now migrating 90% of my mutation logic to Server Actions. The DX improvement and easy optimistic updates outweigh the negligible latency difference. However, for public-facing REST APIs (e.g., used by mobile apps), standard Route Handlers are still required.