The Page That Loaded In 80 Milliseconds
A few months ago a friend sent me a link to a Rails app she'd helped ship at her company. I clicked it on a coffee-shop WiFi connection and the page was fully interactive before I'd put my phone down. No spinner. No skeleton state. No "preparing your dashboard." Just content.
I caught myself doing the thing developers do when they see something genuinely fast: I opened DevTools to figure out what was missing. Nothing was missing. The HTML rendered on the server, included the data, and shipped it. JavaScript loaded after, attached behavior to a few buttons, and went quiet.
This is what server-first feels like — and it's the direction the entire frontend ecosystem has been quietly moving for the last three years. Next.js, React Router 7 (the merged Remix successor, released November 2024), Astro 5, Phoenix LiveView, HTMX, even SvelteKit and SolidStart in their own way. The server is back, and the bundle is smaller for it.
Why The SPA Default Stopped Making Sense
The Single Page Application bargain was: send a big JavaScript bundle once, and every navigation after that is instant. It worked when bundles were 200 KB and users were on desktop fiber.
It stopped working as bundles grew past a megabyte and Core Web Vitals started measuring real users on real phones. A 2 MB JavaScript bundle on a mid-range Android device over a flaky LTE connection takes 8–15 seconds to parse and execute before the user can do anything. The "instant" navigation savings don't matter if the first paint takes 12 seconds.
Then the data-fetching waterfall made it worse. The browser loads HTML, downloads JS, executes JS, mounts React, fires useEffect, requests /api/me, requests /api/feed, requests /api/notifications. Five sequential network round trips before the user sees their actual data. Every one of those round trips has to traverse the public internet from the user's phone.
A server-rendered page does that in one round trip. The user's browser asks for the page. The server, sitting next to the database in the same data center, runs all five queries in parallel over a 1ms network and returns finished HTML. There is no waterfall because the browser was never part of it.
The Frameworks Driving The Shift
Each of the major frameworks took a slightly different path to the same place:
- Next.js App Router — React Server Components and Server Actions. Components run on the server by default. Forms can call server functions directly with
"use server". Released stable with Next 14 in 2023; matured significantly through Next 15 (October 2024). - React Router 7 — the merged Remix and React Router projects, released November 2024. Loaders run on the server, actions handle form submissions, no React Server Components but the same server-first ergonomics. Works against any backend that can serve a Node-shaped runtime.
- Astro 5 — content-driven sites with static HTML by default. The "server islands" feature, added in Astro 5, lets you mark specific components to render on the server per request, while the rest of the page stays static. The result is HTML pages with surgical dynamic regions.
- SvelteKit —
+page.server.tsfiles with server-onlyloadfunctions and form actions. The conventions are different, but the pattern is identical: data fetching runs on the server, the browser receives finished HTML. - HTMX — the radical version. No virtual DOM, no JSX, no client framework at all. The server returns HTML fragments and HTMX swaps them into the page. Pairs with Go, Python, PHP, Ruby — any backend that can render HTML.
Server Actions Are The Quiet Revolution
The piece I find most underrated isn't Server Components — it's the return of the form. For ten years the standard React form looked like this:
function Comment({ postId }: { postId: string }) {
const [text, setText] = useState('');
const [pending, setPending] = useState(false);
async function submit(e: FormEvent) {
e.preventDefault();
setPending(true);
await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text }),
});
setPending(false);
setText('');
}
return (
<form onSubmit={submit}>
<textarea value={text} onChange={(e) => setText(e.target.value)} />
<button disabled={pending}>Post</button>
</form>
);
}
Three pieces of state, a fetch wrapper, an e.preventDefault, and a manual reset. With a Server Action it becomes:
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
async function addComment(formData: FormData) {
'use server';
const text = formData.get('text');
await db.comment.create({ data: { text: String(text), postId: 'abc' } });
revalidatePath('/posts/abc');
}
export function Comment() {
return (
<form action={addComment}>
<textarea name="text" required />
<button>Post</button>
</form>
);
}
No useState. No fetch. No preventDefault. The form posts natively to a server function. If JavaScript loads, the framework intercepts and updates the page without a reload. If JavaScript fails to load, the form still works — the browser does what it has done since 1996.
This is progressive enhancement, baked into the framework as the default rather than as an extra step you have to implement. That alone changes how I write forms now.
What You Actually Stop Writing
The most concrete way to feel the shift is to look at what disappears from a server-first codebase compared to an SPA:
- API routes that exist only to feed your frontend. When a Server Component or loader can hit the database directly, the
/api/usersendpoint with one consumer disappears. - Client-side data libraries. SWR, React Query, Apollo — still useful for genuinely client-driven UIs, but most pages don't need them anymore.
- Loading skeletons and pending states for the initial render. The HTML arrives populated. Skeletons show up only for streamed-in sections or post-mount transitions.
- The hand-rolled
fetchwrapper. When forms call server functions and pages run loaders, the network code shrinks to "the few interactive widgets that genuinely need to talk to the server after the page loaded."
A typical page in the new model has fewer hooks, fewer effects, fewer state machines, and a lot more await.
HTMX Is The Most Honest Take
If you want to feel how far the pendulum has swung, spend an afternoon with HTMX. You write a Go or Python or PHP backend that returns HTML fragments. Buttons on the page have hx-post="/like" attributes. HTMX intercepts the click, posts to the URL, takes the HTML response, and swaps it into the page.
There is no client framework. No virtual DOM. No bundle. No build step. The page is small, fast, and works without JavaScript almost by accident — because if HTMX fails to load, the buttons are still buttons inside forms.
Most teams won't go all the way to HTMX, but it's a useful counterpoint. It proves you can build a modern interactive web app with a fraction of the JavaScript the React ecosystem assumes, and the user can't tell the difference.
Where Client-Heavy Still Wins
Server-first isn't a universal answer. Some apps genuinely need a thick client:
- Real-time editors (Figma, Linear, Notion) — the latency of round-tripping every keystroke to the server is unacceptable. CRDTs, optimistic updates, WebSocket sync.
- Highly interactive canvases (whiteboards, design tools, code editors) — the interaction loop has to run locally.
- Offline-first apps — anything that needs to work without a network has to live in the browser with IndexedDB and a service worker.
For everything else — dashboards, content sites, admin panels, e-commerce, the long tail of CRUD — server-first is the better default. Faster, simpler, less code.
The Shape Of A 2026 Frontend
The honest summary is that "frontend developer" now means "person who writes both halves of a web request." The boundary between server and client is a line you draw inside one component tree, not a contract between two services.
The pages load fast because the browser doesn't have to do impossible work before the first paint. The code is smaller because half the scaffolding it used to need has been deleted. And the end-user experience, on a flaky train connection or a four-year-old phone, is the kind of fast that you stop noticing — which is exactly what the server-first era was always supposed to deliver.





