Why I Switched from Next.js to Vite + React for Client Projects

TL;DR

After 20+ client projects on Next.js, I switched most of my work to Vite + React. The honest reason: most clients don't need SSR, and Next.js makes you pay for SSR even when you're not using it - in build time, in deployment complexity, in mental overhead, and in money. Vite gives me sub-second dev server starts, smaller bundles, deploy-anywhere flexibility, and a stack a junior can hand off in a week. I still reach for Next.js when SEO, edge rendering, or co-located backend code is genuinely the requirement. The rule I now use is: pick the simplest tool that solves the actual problem in front of you, not the most powerful tool in case the problem grows.

The Tipping Point

I've been a Next.js advocate for years. Server-side rendering, file-based routing, API routes, image optimization, the App Router - it's a powerful framework, and Vercel has done a remarkable job of pushing the web forward through it. For long-form content sites, marketing pages, and large multi-tenant SaaS, it's an excellent default.

But after shipping more than twenty client projects across portfolios, dashboards, admin panels, internal tools, and small SaaS products, I noticed an uncomfortable pattern: almost none of my clients actually needed SSR. They needed a fast app, a clean handoff, cheap hosting, and a stack their next developer could understand without a course.

What they were getting was an SSR-shaped framework that added complexity I had to keep explaining away. The first time I shipped a Vite + React + Supabase project end to end and watched the dev server boot in 200ms and the production build deploy to a static host for free, I stopped pretending. For SPAs, Next.js is overkill, and the overkill has a cost.

The Cost of "Just in Case"

Next.js is built around the assumption that some pages will be server-rendered. Even if you push everything to the client, you're still inside that assumption. You pay for it in:

  • A heavier framework runtime in the bundle
  • A Node.js process at deploy time (or an edge function), instead of a static folder
  • A mental model where every component has to remember whether it's "use client" or not
  • Build times that grow super-linearly with the size of the app
  • A development server that, on a Macbook Air, is noticeably slower
  • None of this is fatal. A lot of it is acceptable for the right project. But for an SPA that's going to live behind auth and ship to a handful of users, every one of those costs is being paid for a benefit that's never collected.

    What Vite Gets Right

    Build speed. Vite's dev server starts in under 300 milliseconds on my machine. Cold start. Next.js takes between 3 and 8 seconds on the same hardware. When you're iterating fast - making a small change, checking the browser, making another small change - that gap compounds into real wall-clock hours over a week of work.

    Simplicity. There is no getServerSideProps, no getStaticProps, no generateStaticParams, no decision about which rendering strategy a page should use. Every component is a regular React component. Every route is a regular React Router route. The mental load is dramatically lower, and that frees energy for actual product work.

    Flexibility. A Vite build produces a folder of static files. I can deploy it to Lovable, Netlify, Vercel, Cloudflare Pages, Render, an S3 bucket with CloudFront, or even a cheap shared host. There is no Node.js server to keep alive, no cold start penalty, no "edge runtime" gotchas. For client projects, this matters because the client owns the hosting after handoff and I want their bill to be ten dollars a month, not two hundred.

    Bundle size. A typical Vite + React app I ship is between 100 and 180 KB of JavaScript, gzipped, including the framework. A similar Next.js App Router app starts at around 200 KB before you've written a line of business code. On a mobile connection in Damascus or Khartoum, that gap is the difference between an app that feels instant and one that feels sluggish for the first three seconds.

    Honest dev experience. Hot Module Reload in Vite is essentially perfect - state preserves, edits show up in under 100 milliseconds, and I almost never need to restart the dev server. Next.js has improved a lot here, but it's still noticeably slower and occasionally requires a full restart when something gets confused.

    When I Still Use Next.js

    I don't think Next.js is wrong. I think it's a great tool that's being used for the wrong job most of the time. The cases where I still reach for it:

  • Content-heavy sites that need SEO. Blogs, documentation sites, marketing landing pages, anything where Google needs to crawl the rendered HTML. If your first paint has to be HTML with content, SSR or SSG is the right tool, and Next.js does that well.
  • Apps where first paint matters more than interactivity. Some apps are read-heavy and the JavaScript bundle isn't critical until the user clicks something. Streaming SSR can genuinely help here.
  • Projects that benefit from co-located backend code. When the same engineer writes the API and the UI, Next.js API routes (or Route Handlers in the App Router) genuinely reduce friction.
  • Multi-tenant SaaS with per-tenant subdomains. Edge middleware and dynamic routing are a real win when you have to rewrite or proxy requests based on tenant.
  • If any of those apply, Next.js is the better answer. If none of them apply, Vite is almost certainly the better answer.

    The Stack I've Settled On

  • Vite - build tool and dev server
  • React 18 - UI library
  • TypeScript - type safety, in strict mode by default
  • Tailwind CSS - styling, with shadcn/ui for primitives
  • Supabase - backend (Postgres, auth, storage, edge functions)
  • TanStack Query - data fetching, caching, and mutations
  • React Router - client-side routing
  • Zod - schema validation at the boundary
  • Vitest + Playwright - unit and end-to-end tests
  • This stack covers about 90% of my client work. It is fast to develop in, fast to build, fast to deploy, and - most importantly - easy to hand off to whoever picks up the project next. I've handed off three Vite projects to junior developers and all three were productive within a week.

    A Concrete Comparison

    Here's what a typical "fetch and render" looks like in both stacks. In Next.js App Router:

    // app/projects/page.tsx
    import { db } from '@/lib/db';
    
    export default async function ProjectsPage() {
      const projects = await db.project.findMany();
      return <ProjectsList projects={projects} />;
    }

    This is elegant on the surface, but you've now committed to running a Node process, you've coupled your component tree to a server runtime, and you've added a layer where "this is a server component" vs "this is a client component" becomes a thing you have to think about constantly.

    In Vite + React + TanStack Query:

    // src/pages/Projects.tsx
    import { useQuery } from '@tanstack/react-query';
    import { supabase } from '@/integrations/supabase/client';
    
    export default function ProjectsPage() {
      const { data: projects } = useQuery({
        queryKey: ['projects'],
        queryFn: async () => {
          const { data } = await supabase.from('projects').select('*');
          return data;
        },
      });
      return <ProjectsList projects={projects ?? []} />;
    }

    Slightly more verbose, but everything happens in the browser, every component is the same kind of component, deploys are static files, and the caching/refetching behavior is explicit instead of magical.

    What I Lose

    I'm not going to pretend this is free. Switching to Vite costs you:

  • No server components. You can't render data on the server and skip shipping JavaScript for it. For most SPAs, this doesn't matter, because users are interacting with the app anyway and the JavaScript is already there.
  • Worse out-of-the-box SEO. If SEO matters, you need to add prerendering (I use a build-time script) or move to a different stack. There's no escaping that crawled HTML still beats client-rendered HTML for most search engines.
  • Manual image optimization. No next/image. I usually pre-process images at build time with a script, or use Cloudinary for client uploads.
  • Manual route splitting. You set up React.lazy and Suspense yourself instead of getting it for free. It's about thirty extra lines of code, total.
  • For 90% of the projects I work on, these trade-offs are net positive. For the other 10%, I just use Next.js.

    How to Decide for Your Project

    Ask one question: does my first paint need to be HTML with content?

    If yes - Next.js (or Remix, or Astro). The SSR cost is paying for a real benefit.

    If no - Vite + React. You get a faster dev loop, smaller bundles, cheaper hosting, and a simpler mental model.

    That's the whole decision. Everything else - file routing, image optimization, middleware - is a feature you can add to either stack if you actually need it.

    The Lesson

    The temptation in our industry is to pick the most powerful tool because someday you might need its full power. The reality is most projects never reach "someday." They reach a launch date, they reach a handoff, they reach a sunset.

    The best tool is the simplest one that solves the problem in front of you. For most SPAs, that's Vite + React, and I'm not going back.