Just Fucking Use Next.js: Your Vanilla React SPA Is a Crime Against Humanity

I'm not here to be polite. I'm not here to sugarcoat this @nextjs. I'm not here to gently guide you toward better practices. I'm here because I've watched this industry make the same catastrophic mistakes for years while pretending everything is fine. I'm here because your users are suffering and you don't even realize it. I'm here because somebody needs to tell you the truth, and apparently nobody else has the balls to do it.

Your vanilla React SPA is not clever. It's not minimal. It's not pure . It's a performance nightmare wrapped in a bundle size catastrophe delivered on a foundation of architectural incompetence. And every single day you keep shipping it, you're actively harming real people who made the mistake of trusting you with their time and attention.

This is your intervention. Sit down and pay attention.


THE LIES YOU TELL YOURSELF EVERY MORNING

Your "Simple" SPA Is Actually A War Crime Against User Experience

Let me take you through the actual hell you've created. Not the sanitized version you tell yourself. Not the theoretical best case. The real experience that real humans endure every single time they make the mistake of clicking your link.

Sarah taps your URL. She's on a train going through a tunnel with spotty 3G. Her phone is two years old. Her battery is at thirty percent. She's tired. She just wants to check one thing. One simple thing. Your application is about to ruin her day.

The HTML arrives. Two kilobytes of absolute nothing. A DOCTYPE. An HTML tag. A HEAD with your generic title that says nothing. A BODY with one empty DIV. That's your entire document. That's what you thought was acceptable to send to another human being. An empty box and a promise that maybe, eventually, something will appear.

Her phone sees the script tag for bundle.js and starts downloading. Five point two megabytes. FIVE. POINT. TWO. MEGABYTES. For a webpage. In 2025. On 3G in a tunnel. This will take approximately eight seconds if she's lucky. Twelve if she's not. During this time, what does Sarah see? Nothing. Literally nothing. White screen. Empty page. No content. No indication that anything is happening. No feedback. No progress. Just void.

But you already knew the download would take time, right? That's why you optimized so hard. That's why you tree-shook and code-split and minified. You got it down from six megabytes to five point two. You're a hero. Except Sarah doesn't give a fuck about your optimization efforts. She only cares that she's been staring at nothing for eight seconds and wondering if her internet broke.

The JavaScript finally downloads. Now her phone has to parse it. Parse FIVE MEGABYTES of JavaScript. Her CPU is already thermal throttling because she's been using her phone all day. Parsing takes three more seconds. THREE. MORE. SECONDS. We're at eleven seconds now and Sarah still hasn't seen a single pixel of actual content.

React boots. Virtual DOM constructs. Your App component renders. And then, because you're apparently trying to set a record for bad decisions, your useEffect fires and makes ANOTHER network request. To fetch data. Data that you could have fetched on the server before sending the HTML. Data that was sitting right there next to your server. But no, you decided the right approach was to send empty HTML, wait for JavaScript to download, wait for JavaScript to parse, wait for React to boot, wait for your component to mount, and THEN ask for the data.

The API responds. setState fires. React re-renders. Virtual DOM diffs. Real DOM updates. Paint. Finally. FINALLY. After fourteen seconds, Sarah sees content.

Fourteen seconds. For a webpage. In 2025. And you call yourself a web developer.

"But It Works Fine On My Machine"

Oh, I'm sure it does. I'm absolutely sure it works great on your MacBook Pro with its M2 chip and 32GB of RAM on your office's gigabit fiber connection. I'm sure when you run it locally and test it, everything is smooth as butter. The page loads instantly. The interactions are snappy. The animations are smooth. You think "this is great! I'm a great developer!"

Meanwhile, Sarah is still waiting. Sarah with her mid-range phone on a crowded mobile network. Sarah who just wanted to check one thing real quick. Sarah who is now considering whether your competitor's app might be better because at least theirs loads before the heat death of the universe.

But you don't see Sarah. You see your Lighthouse score running on your development machine on localhost and you think you're winning. You see your bundle size analyzer showing you that you've successfully tree-shaken ten kilobytes out of your five megabyte bundle and you give yourself a pat on the back for being so performance-conscious.

The cognitive dissonance here is staggering. You know mobile users exist. You know most of the world doesn't have gigabit internet. You know most phones aren't flagship devices. But somehow, when you're building your app, you just conveniently forget all of that. It's like you're actively choosing to ignore reality.


THE VANILLA REACT SPA NIGHTMARE THAT YOU PRETEND ISN'T REAL

The Bundle Size Death Spiral

Let me paint you a picture of how your "small React app" becomes a five megabyte monster. You start fresh. You run create-react-app or Vite or whatever the cool kids are using now. You install React. Just React and ReactDOM. About 180 kilobytes total. Not bad, right? You think "I'll keep this lean. I'll only add what I need."

Then you need routing. Of course you need routing. This is a real app. So you install react-router-dom. Another 25 kilobytes. Still not bad. Still manageable. You're a responsible developer.

Then you need to make HTTP requests. You install axios because everyone uses axios. Fifteen kilobytes. You need state management because passing props through five components is getting annoying. You install zustand because it's lighter than Redux. Fifteen more kilobytes. You need forms. You install react-hook-form. Forty-five kilobytes. You need date handling. You install date-fns because everyone says it's smaller than moment. Still adds seventy kilobytes.

And this is just week one. This is before you've built anything real. This is before product management comes to you and says "hey, we need charts for the dashboard". So you install recharts. A hundred and fifty kilobytes. Then they need a rich text editor. You install one of those wysiwyg libraries. Three hundred kilobytes. Then they need to upload images. You install a cropping library. A hundred kilobytes. Then they need animations. You install framer-motion. Two hundred kilobytes.

Six months later you're sitting there wondering how your bundle got to five megabytes and you don't even know what half the dependencies do anymore. You run a bundle analyzer and you see that eighteen different packages all include their own version of lodash utilities. You see that three different date libraries are in there. You see that you're shipping multiple versions of React for some godforsaken reason.

And every single byte of this has to be downloaded by Sarah. Every single byte has to be parsed by her phone's struggling CPU. Every single byte is being shipped to users who just wanted to see a fucking webpage.

The Hydration Horror Show That Haunts Your Dreams

Okay, let me explain hydration to you because I don't think you actually understand what's happening. I don't think you understand the absolute shitshow you've created. Hydration is this magical process where React tries to attach event handlers to HTML that was rendered on the server. Except here's the thing that nobody tells you when you're learning React: hydration is fucking broken by design.

Here's how it's supposed to work. The server renders your React components to an HTML string. It sends that HTML to the browser. The browser displays it immediately so the user sees something while JavaScript downloads. Then JavaScript downloads. React boots up. React renders your entire component tree again in memory. React creates a virtual DOM. Then React compares that virtual DOM to the actual DOM that's on the page. If they match perfectly, React attaches event handlers and everything works. If they don't match, you get a hydration error and everything breaks.

And they won't match. They never match. Because you fucked up somewhere. You used Date.now() somewhere in your component. Server rendered one timestamp, client rendered a different timestamp. Hydration error. You checked window.innerWidth to see if you're on mobile. Server doesn't have a window object. Hydration error. You used Math.random() to generate an id. Different random number on server vs client. Hydration error. You checked localStorage for user preferences. Server doesn't have localStorage. Hydration error.

So what do you do? You wrap everything in useEffect. You create these frankenstein components where half the logic is in the component body and half is in useEffect hooks. You set state on mount to fix the hydration mismatches. You create "mounted" flags that you check everywhere. Your component that should be five lines is now fifty lines and you don't even remember why anymore.

And the best part? The absolute best part? Even when you fix all the hydration errors, you're still shipping the full JavaScript bundle. You're still making Sarah download five megabytes. You're still making her phone parse it all. The only difference is now you're rendering on the server first so she sees something before the page becomes interactive. You've just added complexity without solving the fundamental problem.

The SEO Disaster You're Too Proud To Admit

Let me tell you what Google sees when it crawls your beautiful, modern, cutting-edge React SPA. It sees nothing. It sees an empty div with an id of "root". That's it. That's your entire page to Google.

"But wait," you say, "Google executes JavaScript!" And you're right. Sometimes. Maybe. If it feels like it. If the stars align. If Mercury isn't in retrograde. Google does attempt to execute JavaScript when crawling pages. But here's what you don't understand: Google doesn't wait. Google's crawler is busy. It has billions of pages to crawl. It's not going to sit around for ten seconds waiting for your JavaScript to download and execute and fetch data from your API and render content.

Google gives your page a couple seconds. If nothing meaningful appears in that time, it moves on. It indexes what it saw, which is nothing, and ranks you accordingly. Meanwhile your competitor who actually rendered HTML on the server has their content indexed properly. They have their meta tags set correctly. They have their structured data markup. They have their heading hierarchy. They have their internal links. All of it crawlable, all of it indexable, all of it contributing to their ranking.

And you're sitting there wondering why you're not showing up in search results. You're wondering why you're not getting organic traffic. You're wondering why you have to spend so much on ads. And the answer is simple: you failed at the most basic requirement of the web. Making content accessible to search engines.

But you don't want to admit this because admitting this means admitting that your fancy modern SPA architecture is fundamentally broken for one of the most important use cases on the web. So instead you make excuses. "SEO doesn't matter for our app" you say about your public-facing e-commerce site. "Our users come from direct links" you say while watching your traffic numbers stagnate. "Google will eventually index it" you say while your competitor captures all the organic search traffic.


WHY REACT SERVER COMPONENTS ARE THE PARADIGM SHIFT YOU'RE TOO STUBBORN TO UNDERSTAND

React Server Components: The Architecture That Finally Admits The Browser Is Not A Datacenter

React Server Components exist because after ten years of pretending otherwise, the industry finally admitted a fundamental truth: the browser is a terrible place to do server work. And your SPA has been doing server work in the browser this entire time.

Stop for a second and think about what you're actually doing. You're taking data fetching logic, business logic, authorization logic, data transformation logic, and computational work that should happen on a machine with 32 cores and 128GB of RAM sitting next to the database with sub-millisecond latency, and you're shipping all of it to a phone with 4 cores and 4GB of RAM on a spotty mobile connection hundreds of miles away. This is insane. This has always been insane. We just normalized the insanity.

React Server Components are not SSR. Stop conflating them. SSR renders on the server and then ships everything to the client for hydration. You still send the full application. You still hydrate everything. You still run all the code on the client. SSR is better than pure CSR, but it's a bandaid on a bullet wound.

RSC is fundamentally different. Components marked as server components render ONLY on the server. They never ship to the client. Not their code. Not their dependencies. Not their logic. Nothing. They render to HTML or RSC payload and stream to the browser. The browser receives structured data or HTML. That's it. The component is done. No hydration. No client execution. No JavaScript payload.

Here's what this actually means in practice. You can write a component that queries your database directly. No API endpoint. No fetch call. No loading state. No error boundary for network failures. Just async/await on the database query and return JSX. The query runs on the server where it's fast. The result gets rendered into HTML or RSC payload. The browser receives the rendered output. Done.

The component code never makes it to the client. The database query code never makes it to the client. Your ORM never makes it to the client. Your business logic never makes it to the client. Your secrets never make it to the client. Your bundle is exactly zero bytes bigger for that entire component.

Then you have client components. Components that actually need JavaScript. Buttons with onClick. Forms with state. Animations. Interactive widgets. These get "use client" at the top. These work exactly like React components always have. They ship JavaScript. They run on the client. They're hydrated. They're interactive.

The revolutionary part is composition. Server components can import and render client components. A page can be mostly server components with client components only where interaction is needed. You might have fifty components but only ship JavaScript for five of them. The rest are just HTML rendered on the server.

This is not more complex. This is LESS complex. Server work happens on the server. Client work happens on the client. You're not fighting the framework. You're not working around limitations. You're not shipping megabytes of code to do server work on weak devices. You're just building applications the way they should have been built all along.

Why This Makes Your "But Vanilla Is Simpler" Argument Look Stupid

You know what's actually simple? Writing a component that fetches data and displays it. Here's how you do it with React Server Components:

async function UserList() {
  const users = await db.users.findMany()
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

That's it. That's the whole thing. Async function. Await the database query. Return JSX. Done. No useState. No useEffect. No loading states. No error boundaries. No nothing. Just write the code that makes sense and it works.

Here's how you do the exact same thing in vanilla React SPA:

function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    let cancelled = false
    
    setLoading(true)
    fetch('/api/users')
      .then(r => {
        if (!r.ok) throw new Error('Failed to fetch')
        return r.json()
      })
      .then(data => {
        if (!cancelled) {
          setUsers(data)
          setLoading(false)
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message)
          setLoading(false)
        }
      })
    
    return () => {
      cancelled = true
    }
  }, [])
  
  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

And you call the second one simpler? You call that mess simpler? You have to manage three separate pieces of state. You have to handle cleanup to avoid memory leaks. You have to check if the component is still mounted before setting state. You have to handle loading and error states manually. You have to make a separate API endpoint because you can't just query the database directly. And this is for the simplest possible use case. Loading a list of users.

Now imagine you need to fetch related data. Imagine you need pagination. Imagine you need filtering. Imagine you need real-time updates. Every single one of those requirements adds exponentially more complexity to the vanilla React version while the RSC version barely changes.

But sure. Keep telling yourself vanilla is simpler. Keep writing fifty lines of useEffect spaghetti to do what RSC does in five lines. Keep pretending that managing client-side state for data that never changes on the client is somehow "simpler" than just fetching it on the server.

The Performance Win That Makes Your Arguments Irrelevant

Let me give you some numbers. Real numbers. Not theoretical bullshit. Actual measurements from actual applications.

A typical e-commerce product page built as a vanilla React SPA loads like this: eight seconds until content appears on 3G mobile. Eight. Seconds. During those eight seconds the user sees a white screen or maybe a loading spinner if you're fancy. The JavaScript bundle is 3.2 megabytes. The initial HTML is 2 kilobytes. Time to interactive is ten seconds because even after content appears, the page isn't fully interactive yet.

The same exact product page built with Next.js and React Server Components loads like this: one point two seconds until content appears on 3G mobile. The JavaScript bundle for the page is 180 kilobytes because most of the components are server components that don't ship JavaScript. The initial HTML is 85 kilobytes and contains the full product information. Time to interactive is one point eight seconds.

That's not a small difference. That's not a "well technically it's faster but users won't notice" difference. That's the difference between users completing purchases and users abandoning your site. That's the difference between good Core Web Vitals and failed Core Web Vitals. That's the difference between ranking in search results and being invisible.

And the crazy thing? The Next.js version was easier to build. The code is simpler. There's less of it. There are fewer bugs. It's more maintainable. You win on every metric. Performance, developer experience, user experience, SEO, accessibility. Every single one.


THE REACT2SHELL SECURITY INCIDENT AND WHY YOUR PANIC PROVED YOU UNDERSTAND NOTHING

What Actually Happened And Why It Matters

Late 2025. Security researcher discovers a vulnerability in React Server Components. Real vulnerability. Maximum severity. Remote code execution under specific conditions. The kind of CVE that makes security teams sweat. It involved JavaScript function constructors, serialization boundaries, and server execution contexts. Deep computer science shit that most developers don't understand.

The internet lost its mind. Twitter exploded. "RSC is broken!" "Next.js is dangerous!" "This is why frameworks are bad!" "Go back to SPAs!" Every developer who had been looking for an excuse to avoid learning new things suddenly had their ammunition. They shared that CVE like it was gospel proof that they were right all along.

These people are idiots. And I'm going to explain exactly why.

SPAs feel safe because they're weak. They feel safe because they don't do anything meaningful on the server. They dump all responsibility on the client. That's not security. That's abdication. If your server is so simple it can't be exploited, it's probably not doing anything worth protecting. It's probably just serving static files while your client does all the real work in the most insecure environment imaginable: random people's browsers.

Every powerful system has vulnerabilities. Linux has had thousands of kernel vulnerabilities. Are you going to stop using Linux? PostgreSQL has had serious security issues. Are you going to stop using databases? Chrome has had hundreds of critical security bugs. Are you going to stop using browsers? OpenSSL had Heartbleed. Are you going to stop using encryption?

No. Because you're not a fucking idiot. You understand that complex systems have bugs. You understand that vulnerabilities get discovered and patched. You understand that the question is not whether bugs exist but how the ecosystem responds to them.

How Vercel Responded Like An Infrastructure Company That Knows What The Fuck They're Doing

Here's what actually happened. Before public disclosure, Vercel coordinated with every major cloud provider. AWS. Google Cloud. Azure. Cloudflare. Netlify. Fastly. Deno. They deployed mitigations across the entire infrastructure. When the CVE went public, the protections were already live. Millions of Next.js applications were automatically protected without their developers doing anything.

In the first twenty-four hours after public disclosure, over two million exploit attempts were automatically blocked. TWO MILLION. That's not panic. That's not scrambling. That's infrastructure working exactly as it should.

But they didn't stop there. They launched a bug bounty challenge. Public. Open to anyone. Fifty thousand dollars per bypass of the mitigations. Over a hundred security researchers participated. Over a hundred vulnerability reports submitted. Dozens of real issues found and fixed. One million dollars paid out to researchers.

Read that again. They paid out ONE MILLION DOLLARS to people trying to break their system. They INVITED attacks. They REWARDED people who found weaknesses. This is not the behavior of a company that's scared or unprepared. This is confidence. This is mature engineering. This is how real infrastructure companies operate.

Then they went deeper. Runtime protections built directly into the framework. Dangerous execution paths blocked during React rendering. Defense in depth. Multiple layers. Not just network-level WAF bandaids. Actual code-level protections that prevent entire classes of vulnerabilities.

What This Actually Proves About You

Your reaction to React2Shell proved something important. It proved you don't actually care about security. You care about being right. You care about validating your choice to avoid learning new things. You care about having ammunition against technologies you decided you don't like.

If you actually cared about security, you would have praised the response. You would have recognized this is exactly how serious vulnerabilities should be handled. Responsible disclosure. Coordinated mitigation. Proactive security research. Runtime protections. This is the gold standard.

But you didn't praise it. You used it as an excuse. An excuse to keep building shitty SPAs. An excuse to avoid RSC. An excuse to tell everyone you were right to be skeptical. You wanted to be right more than you wanted to understand what actually happened.

The React2Shell vulnerability and response is actually an argument FOR using Next.js and RSC, not against it. It proves the ecosystem is mature. It proves the team takes security seriously. It proves the infrastructure is robust enough to handle serious threats. It proves that when vulnerabilities are discovered, they get fixed properly.

Meanwhile your SPA is probably riddled with XSS vulnerabilities, CSRF issues, and authentication bugs that you don't even know about because you rolled your own everything and nobody's looking at your code.


THE "SLOW LINK" MYTH: WHY YOUR PANIC PROVED YOU DIDN'T READ THE DOCS

“Clicking a link and waiting multiple seconds” is a skill issue, not a framework issue.

I see the complaints. I see the tweets. I see the people crying because they clicked a link and it took seconds to transition. And every time I see it, I know exactly what happened: you didn't read the fucking documentation.

Next.js gives you every tool to make transitions instant. loading.js for instant feedback. suspense for granular streaming. Prefetching by default for every link in the viewport. If your transition is slow, it's because you're doing blocking work on the server without providing a loading state.

Experience An Instant Transition

Try it. Click it. It's instant. Because we actually used the platform.


THE FINAL VERDICT THAT YOU NEED TO HEAR

Next.js Won. The Debate Is Over. Your Resistance Is Pointless.

The numbers are in. The data is conclusive. The results are undeniable. Next.js with React Server Components is objectively, measurably, provably superior to vanilla React SPAs for building modern web applications. This is not opinion. This is not preference. This is not taste. This is documented reality backed by performance metrics, business outcomes, and user experience data from thousands of production applications.

Every single team that migrates from SPAs to Next.js with RSC reports the same results. Load times cut by seventy to eighty percent. Bundle sizes reduced by eighty to ninety percent. Time to interactive improved by seventy to eighty percent. Lighthouse scores jump from failing to excellent. Core Web Vitals transform from red to green. SEO traffic increases. Conversion rates improve. User complaints decrease. Development velocity increases. Bug counts decrease. Developer satisfaction improves.

Your SPA is killing your application.

Your stubbornness is killing your career.

Your pride is killing your users' patience.

Just fucking use Next.js.

Prime out.


GET STARTED RIGHT NOW

Stop reading. Start building.

bun create next-app@latest my-app --yes
cd my-app  
bun dev

Open http://localhost:3000 and welcome to the future.


LEARN MORE

Official Documentation: nextjs.org/docs
React2Shell Security Response: vercel.com/blog
React Server Components: react.dev/reference
Next.js Examples: github.com/vercel/next.js
Production Case Studies: nextjs.org/showcase

ABOUT

Created with love and rage by the team at Render

Inspired by justfuckingusereact.com

Twitter: @infinterenders
GitHub: github.com/renderhq
Love Letter to Next.js: Read the thread

This is an intervention. This is the wake up call the web development community desperately needs. This is your last chance to stop building garbage and start building applications that don't make your users want to throw their phones out the window.

Stop reading rants. Start building applications. The tools are ready. The documentation is clear. The community is massive. There is no excuse anymore.

Just fucking use Next.js.