The Death of the "Zombie" Button: Why Server Actions are the New Baseline for Web Performance

Search for a command to run...

No comments yet. Be the first to comment.
Building Dream Decoder taught me that not all React Native changes are equal. Some need a new binary submitted to the store. Others can go straight to users in minutes. The difference comes down to on
What Anthropic tested, what they didn't, and three possible futures for AI autonomy
When an LLM generates text, it isn't thinking in sentences. It computes a massive list of raw scores (logits) for its entire vocabulary, converts them into a probability distribution, and selects the

Cron Not Working on Mac? How to Fix the macOS Sleep Trap with launchd
A conversation-driven guide to bias, variance, bagging, and boosting for engineers who've read the dartboard analogy five times and still aren't sure what to do about it.

We’ve all been there. You’re on your commute, the network connection is patchy, and you try to submit a form on a sleek, modern website.
You tap the bright blue “Submit” button.
Nothing happens.
You tap it again. Still nothing. The button is completely dead.
Frustrated, you close the tab. Just like that, the company loses a lead.
Ironically, this isn’t a backend failureit’s a frontend one.
Their state-of-the-art React app simply couldn’t handle a slow 3G connection.
For years, we accepted this as the cost of doing business in the Single Page Application (SPA) era.
But Next.js Server Actions change the equation completely.
Let’s cut through the hype and look at the actual data—the bundle sizes, the network behavior, and why moving mutation logic back to the server is one of the most resilient decisions you can make.
To understand why Server Actions matter, we have to look at what it actually costs to build a robust form on the client side using standard API routes. If you are building a production-grade form today, you aren't just writing a fetch call; you are bringing in an entire ecosystem.
React Hook Form (State management): ~5kB
Zod (Schema validation): ~12kB
React Query / SWR (Mutation & Cache handling): ~13kB
Custom Logic & Fetch Boilerplate: ~5-10kB
Total Added Payload: ~35kB to 40kB (Gzipped)
On fiber internet, 40kB is a blink. On a mid-range Android phone over 3G, that JavaScript can add 1.5 to 3 seconds to your Time to Interactive (TTI). During those seconds, the user sees the form, but the button is a "zombie." If they click it, the event is lost. This is the Hydration Gap.
When you use a Server Action, your mutation logic bundle size drops to 0kB.
Because the action runs entirely on the server, you don't ship Zod or your fetch boilerplate to the client. Next.js compiles the action into a tiny, encrypted action ID inside a native HTML <form>.
First Contentful Paint (FCP): Happens almost instantly because you're shipping lean HTML.
Time to Action: Because the browser uses a native HTML POST request, the user can click "Submit" before the JavaScript even finishes downloading. The request travels to the server while the browser is still busy hydrating.
Eliminating the Waterfall: Instead of the typical "POST -> Success -> GET updated data" sequence, Server Actions allow the server to mutate the data and return the updated UI in a single network roundtrip.
A common fear is that moving logic to the server means losing the "snappiness" of modern apps. React 19 hooks bridge this gap perfectly.
Using useFormStatus, you can create a reusable SubmitButton that automatically knows when its parent form is pending. No more manual setIsLoading(true) states scattered across your components.
useOptimisticFor "Likes" or "Comments," you don't want to wait for the server. The useOptimistic hook allows you to "fake" a successful response in the UI immediately. If the server eventually fails, React automatically rolls back the UI state.
When one field depends on another, you have two high-performance choices:
The URL Strategy: Update a query param (?country=nepal). The Server Component reads this and returns the filtered "State" list as pure HTML.
The Hybrid Fetch: Use a simple client-side fetch to hit a Route Handler for the dependent data, while keeping the final form submission as a Server Action for type safety and security.
useActionStateHere is how this looks in practice. Notice that zod is imported in the server file—it never touches the client's browser.
actions.ts)'use server'
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
const ContactSchema = z.object({
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContact(prevState: any, formData: FormData) {
const validated = ContactSchema.safeParse(Object.fromEntries(formData));
if (!validated.success) {
return { errors: validated.error.flatten().fieldErrors, data: validated.data };
}
// Database mutation logic here
await db.message.create({ data: validated.data });
revalidatePath('/contact');
return { success: true };
}
ContactForm.tsx)'use client'
import { useActionState } from 'react';
import { submitContact } from './actions';
export default function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, null);
return (
<form action={formAction}>
<input name="email" defaultValue={state?.data?.email} />
{state?.errors?.email && <p className="error">{state.errors.email}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Submit'}
</button>
</form>
);
}
Server Actions are a powerful default, but they are not a silver bullet. Senior engineers know when to stay on the client:
High-Frequency UI: Range sliders, drawing canvases, or search-as-you-type interfaces need to run at 60fps. The network latency of a Server Action would make these feel broken.
Offline-First Apps: If your app must work in a tunnel (e.g., a field-service app), you need a client-side database and local mutations.
Massive File Uploads: Serverless functions (like Vercel) often have a 4.5MB payload limit. For 100MB videos, use direct-to-S3 presigned URLs instead.
Public APIs: Webhooks or mobile app endpoints still require standard Route Handlers (app/api/...), as Server Actions use encrypted IDs that external tools cannot call.
We don't adopt Server Actions just because they're the "Next Big Thing." We adopt them because eliminating 40kB of mutation logic directly correlates to a lower Time to Interactive. By relying on native HTML forms as a baseline, we build applications that are inherently more resilient.
No matter how bad a user's internet connection gets, your "Submit" button should actually work.