Skip to content

When Server-Side Rendering Is Better Than SPA: A Honest Comparison

The Problem

I deployed my first production SPA (Single Page Application) and checked Google Search Console the next day.

Google Search Console results
Indexed pages: 0
Crawl errors: 47
Average load time: 4.2s

My blog had 50 articles. Zero were indexed.

I had spent weeks building a beautiful React SPA with smooth transitions, client-side routing, and all the modern conveniences. But Google couldn’t see any of it.

This is the story of what went wrong and what I learned about SPA disadvantages.

What Happened

My SPA looked like this to search engines:

index.html (what Google sees)
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>

Empty. Just an empty div and a 2MB JavaScript bundle.

To a human visitor, the page would eventually show beautiful content. But to Google’s crawler, it was a blank page. The crawler would request the HTML, see nothing, and move on.

The Double-Load Problem

Here’s what really frustrated me about SPAs:

SSR Timeline (ideal):

SSR loading timeline
t=0ms Browser requests HTML
t=300ms Browser receives HTML with content
User sees content immediately

SPA Timeline (reality):

SPA loading timeline
t=0ms Browser requests HTML
t=100ms Browser receives empty HTML
t=100ms Browser requests bundle.js (2MB)
t=500ms Browser parses JavaScript
t=500ms JavaScript requests /api/articles
t=700ms User finally sees content

My users were waiting 700ms to see what could have been shown in 300ms. That’s a 400ms penalty just to use React.

One Reddit commenter put it perfectly:

“It’s a page that loads and then loads again.”

The Real SPA Disadvantages

After researching and discussing with other developers, I found these are the real problems:

1. SEO Is Hard (Not Impossible, But Hard)

Google claims it can execute JavaScript. Technically true, but:

  • Crawling JS is slower and more resource-intensive
  • Google allocates less crawl budget for JS-heavy pages
  • Some search engines (DuckDuckGo, older bots) don’t execute JS at all
  • Social media previews (Open Graph) need extra setup

I tried adding react-helmet for meta tags:

Attempting to fix SEO
import { Helmet } from 'react-helmet'
function BlogPost({ post }) {
return (
<>
<Helmet>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
</Helmet>
<article>{post.content}</article>
</>
)
}

It helped a bit. But my social shares still showed blank previews until I added pre-rendering.

2. URL Handling Gets Messy

I broke my back button. Multiple times.

URL handling problem
User clicks: Home -> Article -> Comments
User presses Back
Result: Goes to Home instead of Article

The problem? My client-side routing wasn’t properly syncing with browser history.

And page refreshes? Half the time they’d show a 404 because my server wasn’t configured for client-side routing fallback.

3. JavaScript Bundle Size

My bundle kept growing:

The imports that killed my bundle size
import { Dashboard } from './Dashboard' // 50KB
import { Settings } from './Settings' // 30KB
import { AdminPanel } from './AdminPanel' // 80KB
import { Analytics } from './Analytics' // 120KB
// ... total: 2.3MB before gzip

Code splitting helped, but users still had to download a significant chunk before anything appeared.

4. Crawler and LLM Access

Here’s something I didn’t expect: ChatGPT and Claude couldn’t summarize my articles.

Why? Their browsing tools struggle with SPAs:

  • They see the empty HTML first
  • Some don’t execute JavaScript at all
  • Those that do have timeout limits

My content was invisible to the fastest-growing source of traffic: AI assistants.

5. Security: Information Leakage

I discovered my API was exposing too much:

What my API returned
{
"user": {
"id": 1,
"role": "admin",
"permissions": ["delete_users", "view_analytics", "impersonate_users"]
}
}

Any user could inspect the network tab and see these admin endpoints. With SSR, I could render based on permissions without ever sending the permission list to the client.

When SSR Is Clearly Better

I rebuilt my blog with SSR. Here’s where it makes a huge difference:

ScenarioWhy SSR Wins
E-commerceSEO critical, fast load drives conversions
Blog/NewsMust be indexed, social sharing previews
Marketing sitesLoad speed affects bounce rate
Public docsSearch visibility, AI accessibility

The common theme: public content that needs to be discovered.

When SPA Is Still the Right Choice

I’m not saying SPAs are bad. They’re the right choice for:

ScenarioWhy SPA Works
Internal dashboardsAuthenticated users, no SEO needed
Admin panelsComplex interactivity, real-time updates
SaaS appsBehind login, users expect app-like experience
PrototypesSpeed of development matters most

If your app is behind a login screen, SPA is perfectly fine. Google won’t see it anyway.

The Hybrid Solution

You don’t have to choose extremes. My current approach uses both:

Option 1: Next.js Mixed Rendering

Next.js hybrid approach
// Marketing pages: Static (pre-rendered at build time)
export const dynamic = 'force-static'
export default function MarketingPage() {
return <div>Static content, indexed by Google</div>
}
Client-side for interactive parts
'use client'
function Dashboard() {
const [data, setData] = useState(null)
// SPA-style interactivity
}

Option 2: Astro for Content

Astro islands architecture
---
import Layout from '../layouts/Layout.astro'
---
<Layout>
<h1>Static HTML, indexed by Google</h1>
<InteractiveChart client:load /> <!-- Hydrated only when needed -->
</Layout>

Astro is great for this. It ships zero JavaScript by default and hydrates only what needs interactivity.

The Performance Comparison

I measured both approaches on my blog:

Performance comparison
SPA (React):
- First Contentful Paint: 1.8s
- Time to Interactive: 3.2s
- Lighthouse Score: 62
SSR (Next.js):
- First Contentful Paint: 0.4s
- Time to Interactive: 1.1s
- Lighthouse Score: 94
Static (Astro):
- First Contentful Paint: 0.2s
- Time to Interactive: 0.3s
- Lighthouse Score: 98

The difference isn’t subtle.

The Security Fix

With SSR, I stopped leaking sensitive data:

Before: SPA API
app.get('/api/user', (req, res) => {
res.json(req.user) // Contains role, permissions, secrets
})
After: SSR approach
// Server-side rendering decides what to show
function Dashboard({ user }) {
// user only contains { id, name }
// permissions checked server-side
// client never sees admin endpoints
}

The admin panel renders server-side. The user never sees the API structure for admin operations.

What I Learned

The Reddit debate I found was polarized:

“SPA is a relic of the past. It was a necessity, not something that was embraced because it’s so cool.”

Another comment balanced it:

“If you have complex auth, need SEO, or have a huge app, SPA is not the best choice. But there are many use cases where SPA is the right choice.”

The honest answer isn’t “SPA is dead” or “Next.js is overkill.”

It’s: What does your project actually need?

My Decision Matrix

I now use this simple checklist:

Decision matrix
Is your content public? Yes -> Consider SSR/Static
Does SEO matter? Yes -> SSR/Static required
Is it behind login? Yes -> SPA is fine
Do you need real-time updates? Yes -> SPA or hybrid
Is initial load speed critical? Yes -> SSR/Static
Do users expect app-like UX? Yes -> SPA or hybrid

For my blog: SSR/Static wins. For my admin dashboard: SPA wins. For my SaaS app: Hybrid (Next.js with client components).

Summary

SPAs have real disadvantages: SEO challenges, slower initial perceived load, URL handling issues, and crawler accessibility problems. SSR/SSG is better for public content, e-commerce, and SEO-dependent sites. SPA is still the right choice for authenticated apps, dashboards, and tools where interactivity matters more than search visibility.

The key is matching the technology to the use case, not blindly following trends.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments