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.
Indexed pages: 0Crawl errors: 47Average load time: 4.2sMy 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:
<!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):
t=0ms Browser requests HTMLt=300ms Browser receives HTML with content User sees content immediatelySPA Timeline (reality):
t=0ms Browser requests HTMLt=100ms Browser receives empty HTMLt=100ms Browser requests bundle.js (2MB)t=500ms Browser parses JavaScriptt=500ms JavaScript requests /api/articlest=700ms User finally sees contentMy 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:
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.
User clicks: Home -> Article -> CommentsUser presses BackResult: Goes to Home instead of ArticleThe 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:
import { Dashboard } from './Dashboard' // 50KBimport { Settings } from './Settings' // 30KBimport { AdminPanel } from './AdminPanel' // 80KBimport { Analytics } from './Analytics' // 120KB// ... total: 2.3MB before gzipCode 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:
{ "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:
| Scenario | Why SSR Wins |
|---|---|
| E-commerce | SEO critical, fast load drives conversions |
| Blog/News | Must be indexed, social sharing previews |
| Marketing sites | Load speed affects bounce rate |
| Public docs | Search 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:
| Scenario | Why SPA Works |
|---|---|
| Internal dashboards | Authenticated users, no SEO needed |
| Admin panels | Complex interactivity, real-time updates |
| SaaS apps | Behind login, users expect app-like experience |
| Prototypes | Speed 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
// 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>}'use client'
function Dashboard() { const [data, setData] = useState(null) // SPA-style interactivity}Option 2: Astro for Content
---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:
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: 98The difference isn’t subtle.
The Security Fix
With SSR, I stopped leaking sensitive data:
app.get('/api/user', (req, res) => { res.json(req.user) // Contains role, permissions, secrets})// Server-side rendering decides what to showfunction 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:
Is your content public? Yes -> Consider SSR/StaticDoes SEO matter? Yes -> SSR/Static requiredIs it behind login? Yes -> SPA is fineDo you need real-time updates? Yes -> SPA or hybridIs initial load speed critical? Yes -> SSR/StaticDo users expect app-like UX? Yes -> SPA or hybridFor 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