Just Use Next.js: Your Vanilla React SPA Is Failing Your Users

This is your intervention.

Your vanilla React SPA ships 5MB of JavaScript to users on 3G networks. They stare at blank screens for 14 seconds waiting for content that should have been in the initial HTML. While you're testing on your MacBook Pro with gigabit fiber, real users on mid-range phones are abandoning your site.

Every single day you keep shipping this architecture, you're actively harming real people who trusted you with their time and attention. Here's why, and what to do about it.


The Real Cost of Client-Side Rendering

The real user experience: Sarah taps your link on a train with spotty 3G. Her phone is two years old. Battery at 30%. She just wants to check one thing. Your app sends her 2KB of empty HTML—a DOCTYPE, an HTML tag, and one empty div. Then she waits 8 seconds downloading 5.2MB of JavaScript. Another 3 seconds parsing it on her thermal-throttling CPU. React boots. useEffect fires. Another API request. Finally, after 14 seconds, she sees content.

The waterfall: User requests page Downloads 2KB empty HTML Waits for 5MB JavaScript Parses JavaScript React boots useEffect fires Fetches data from API Finally sees content.

On 3G mobile (50% of global traffic): 14+ seconds to content. On 4G: 6-8 seconds. Industry research shows 53% of users abandon sites that take over 3 seconds to load.

Bundle Size Compounds Fast

React + ReactDOM: 180KB. React Router: +25KB. Axios: +15KB. Zustand: +15KB. React Hook Form: +45KB. date-fns: +70KB. Recharts: +150KB. Rich text editor: +300KB. Image cropper: +100KB. Framer Motion: +200KB.

Result after 6 months: 5MB bundle with duplicate dependencies. Bundle analyzers show 18 packages shipping their own lodash. Three date libraries. Multiple React versions.

SEO Reality

Google's crawler sees: <div id="root"></div>

Googlebot waits 2-5 seconds max. If content doesn't appear, it indexes nothing. Your competitor with server-rendered HTML gets indexed with proper meta tags, structured data, internal links, and heading hierarchy.

Business impact: Zero organic traffic. Total dependence on paid ads. Lost revenue from discoverable content.


How React Server Components Fix This

Core principle: Server work happens on the server. Client work happens on the client. Not SSR. Not hydration. Something fundamentally different.

RSC is not SSR. SSR renders on the server 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.

RSC is fundamentally different. Server Components render ONLY on the server. Their code, dependencies, and logic 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. Zero bytes in your bundle.

Client Components (marked with "use client") work exactly like traditional React components. They ship JavaScript, run in the browser, and handle interactivity. Use them only where interaction is needed.

The breakthrough: Compose them together. Build pages that are 90% Server Components with Client Components only where interaction is needed. Query databases directly. No API endpoints. No loading states. No hydration mismatches. Just async/await and return JSX.

Code Comparison

Server Component (Next.js):

async function ProductPage({ params }) {
  const product = await db.products.findById(params.id)
  const related = await db.products.findRelated(product.category)
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
      <AddToCartButton productId={product.id} />
      <RelatedProducts items={related} />
    </div>
  )
}

Database queries run on the server. No API endpoints. No loading states. No error boundaries for network failures. Just async/await and return JSX. AddToCartButton is the only Client Component.

Vanilla React SPA equivalent:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null)
  const [related, setRelated] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    let cancelled = false
    
    Promise.all([
      fetch(`/api/products/${productId}`),
      fetch(`/api/products/${productId}/related`)
    ])
      .then(([p, r]) => Promise.all([p.json(), r.json()]))
      .then(([productData, relatedData]) => {
        if (!cancelled) {
          setProduct(productData)
          setRelated(relatedData)
          setLoading(false)
        }
      })
      .catch(err => {
        if (!cancelled) setError(err.message)
      })
    
    return () => { cancelled = true }
  }, [productId])
  
  if (loading) return <Spinner />
  if (error) return <Error message={error} />
  if (!product) return <NotFound />
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
      <AddToCartButton productId={product.id} />
      <RelatedProducts items={related} />
    </div>
  )
}

State management. Cleanup logic. Loading states. Error handling. Race condition prevention. Separate API endpoints that still query the same database. All this complexity to fetch data that was sitting on the server next to your code.

Real Performance Data

E-commerce product page, measured on 3G mobile (Global Median):

Vanilla React SPA:

  • First Contentful Paint: 8.2s
  • Largest Contentful Paint: 9.1s
  • Time to Interactive: 10.3s
  • Total JavaScript: 3.2MB
  • Initial HTML: 2KB

Next.js + Server Components:

  • First Contentful Paint: 1.2s
  • Largest Contentful Paint: 1.6s
  • Time to Interactive: 1.8s
  • Total JavaScript: 180KB
  • Initial HTML: 85KB (with content)

Impact: 85% reduction in load time. 94% reduction in JavaScript. Users see content 7 seconds faster. Completed purchases increase 40-60%. Bounce rate drops 50%.

Additional Benefits

Automatic code splitting: Each route only loads its JavaScript. No manual configuration.

Streaming SSR: Send HTML as it's generated. Users see content progressively.

Built-in optimizations: Image, font, and script optimization all automatic.

Simplified data fetching: No Redux, no context hell, no prop drilling.


Security Done Right

React2Shell CVE happened. Maximum severity. Remote code execution. The kind that makes security teams sweat. The internet panicked. "RSC is broken!" "Next.js is dangerous!" Every developer looking for an excuse to avoid learning new things suddenly had their ammunition.

Here's what actually happened:

  • Coordinated disclosure with all major cloud providers before public announcement
  • Automatic protection for millions of applications with zero developer action required
  • 2M+ exploit attempts automatically blocked in first 24 hours
  • Public bug bounty: $50K per mitigation bypass
  • $1M paid to security researchers who found and reported issues
  • Runtime protections built into framework core

This is how serious infrastructure companies handle security. This is the gold standard. Responsible disclosure. Coordinated mitigation. Proactive security research. Runtime protections. Meanwhile your custom SPA backend probably hasn't had a security audit and is riddled with XSS vulnerabilities you don't even know about.

The React2Shell response is an argument FOR using Next.js, not against it. It proves the ecosystem is mature. It proves the team takes security seriously. It proves that when vulnerabilities are discovered, they get fixed properly.


Migration Results

Teams migrating from SPAs to Next.js consistently report:

Load times -75%
Bundle sizes -85%
Time to Interactive -75%
Lighthouse scores 90-100
Core Web Vitals Green
Organic traffic +300%
Conversion rates +50%
Dev velocity +40%

This isn't theory. This is measured data from production applications serving millions of users.


The Verdict

The numbers are in. The data is conclusive. Next.js with React Server Components is objectively, measurably, provably superior to vanilla React SPAs for building modern web applications.

Every team that migrates reports the same results: 75% reduction in load times. 85% reduction in bundle sizes. Lighthouse scores jump from failing to excellent. SEO traffic increases. Conversion rates improve. Development velocity increases.

Your SPA is killing your application. Your stubbornness is killing your career. Your pride is killing your users' patience.

Just use Next.js.


Get Started

npx create-next-app@latest
# or
bun create next-app@latest

cd my-app
npm run dev

Open localhost:3000

File-based routing: Create app/about/page.js automatic route at /about

Server Components by default: No configuration needed

Add Client Component: Add "use client" at top of file

Deploy: vercel deploy or any platform supporting Node.js


Resources

Official Documentation
nextjs.org/docs

Complete guide to App Router and Server Components

React Server Components
react.dev/reference

React team's official explanation

Examples Repository
github.com/vercel/next.js

300+ working examples for every use case

Production Showcases
nextjs.org/showcase

Companies using Next.js at scale


Created by @infinterenders • Inspired by justfuckingusereact.com

TwitterGitHub