SSR And Prerendering · How it works
2 min readRapid overview
- How it works
- Core Concepts
- What Is Server-Side Rendering (SSR)?
- What Is Pre-rendering (Static Site Generation)?
- The Reality: Why SSR Is Often Overkill
- 1. Requires Maintenance and Development
- 2. Consumes Server Compute Time
- 3. Doesn't Produce 1:1 Result (Cloaking Concerns)
- 4. Each Request Served from Origin
- 5. Still Requires Hydration
- Implementation Complexity Comparison
- Simple SPA (Minutes to Deploy)
- SSR Application (Days to Production-Ready)
How it works
Core Concepts
What Is Server-Side Rendering (SSR)?
- Dynamic HTML Generation: Server generates HTML for each request
- Full Page Delivered: Complete HTML sent to client before JavaScript loads
- SEO Friendly: Search engines can crawl content immediately
- Framework Support: Next.js, Nuxt.js, Angular Universal, etc.
What Is Pre-rendering (Static Site Generation)?
- Build-Time Generation: HTML generated during build process
- Static Files: Pre-built HTML served from CDN
- Faster Response: No server computation per request
- Limited Dynamism: Content is static until next build
The Reality: Why SSR Is Often Overkill
1. Requires Maintenance and Development
SSR adds significant complexity to your application architecture:
// Without SSR - simple client-side rendering
function ProductPage() {
const [product, setProduct] = useState(null);
useEffect(() => {
fetchProduct(id).then(setProduct);
}, [id]);
return <ProductDisplay product={product} />;
}
// With SSR - additional server-side logic required
export async function getServerSideProps(context) {
// Must handle: authentication, cookies, headers
// Must manage: database connections, caching
// Must consider: error handling, timeouts
const product = await fetchProduct(context.params.id);
return {
props: { product }
};
}
function ProductPage({ product }) {
return <ProductDisplay product={product} />;
}
Additional maintenance burden:
- Server environment setup and monitoring
- Node.js version management
- Memory leak detection
- Cold start optimizations
- Error logging and debugging on server
2. Consumes Server Compute Time
Every request requires server processing:
Client Request → Server Processing → HTML Generation → Response
Traditional SPA:
- Server: Serve static files (fast, cheap, cacheable)
- Cost: Minimal
SSR:
- Server: Execute React/Vue, fetch data, render HTML
- Cost: CPU time, memory, database connections per request
Cost implications:
- Serverless functions charge per execution time
- More server instances needed for traffic spikes
- Database connection pooling becomes critical
- Higher infrastructure costs overall
3. Doesn't Produce 1:1 Result (Cloaking Concerns)
The HTML delivered to search engines may differ from what users actually see:
// Server renders initial state
export async function getServerSideProps() {
const products = await getProducts(); // Gets 10 products
return { props: { products } };
}
// Client may show different content
function ProductList({ products }) {
const [filteredProducts, setFiltered] = useState(products);
const [userLocation, setUserLocation] = useState(null);
useEffect(() => {
// Client-side personalization changes displayed content
getUserLocation().then(location => {
setUserLocation(location);
setFiltered(products.filter(p => p.availableIn(location)));
});
}, []);
// Users see filtered/personalized content
// Search engines indexed the unfiltered version
return <Grid products={filteredProducts} />;
}
Cloaking risks:
- Search engines may see different content than users
- Personalized content not reflected in indexed version
- A/B tests show different variants to bots vs users
- Dynamic pricing shows different prices to crawlers
4. Each Request Served from Origin
Unlike static files that can be served entirely from CDN edge:
Static SPA:
User → CDN Edge (< 50ms) → Cached HTML/JS
SSR:
User → CDN Edge → Origin Server (100-500ms) → Database → Response
↑ Geographic latency
↑ Server processing time
↑ Database query time
Performance implications:
- Users far from server experience latency
- Cold starts in serverless add delay
- Database becomes a bottleneck
- CDN caching limited (dynamic content)
5. Still Requires Hydration
SSR doesn't eliminate JavaScript - it delays it:
<!-- Server sends this HTML -->
<div id="root">
<button class="counter">Count: 0</button>
<p>Click the button to increment</p>
</div>
<!-- But the button doesn't work until... -->
<script src="/app.js"></script>
<!-- ...JavaScript loads and "hydrates" -->
// Hydration process
function App() {
const [count, setCount] = useState(0);
// After hydration, button finally becomes interactive
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
// React attaches event handlers to existing HTML
hydrateRoot(document.getElementById('root'), <App />);
Hydration problems:
- Time to Interactive (TTI): Users see content but can't interact
- Hydration mismatch: Server/client differences cause errors
- Double rendering: Component logic runs twice
- Bundle size unchanged: Still need full JavaScript bundle
- "Uncanny Valley": Page looks ready but isn't responsive
Implementation Complexity Comparison
Simple SPA (Minutes to Deploy)
npm create vite@latest my-app -- --template react
cd my-app
npm run build
# Deploy dist/ folder to any static host
SSR Application (Days to Production-Ready)
// Server setup
import express from 'express';
import { renderToString } from 'react-dom/server';
const app = express();
app.get('*', async (req, res) => {
try {
// Data fetching
const data = await fetchDataForRoute(req.url);
// HTML generation
const html = renderToString(<App data={data} />);
// Template injection
const fullHtml = template.replace('<!--app-->', html);
res.send(fullHtml);
} catch (error) {
// Error handling
res.status(500).send('Server Error');
}
});
// Plus: process management, logging, monitoring,
// health checks, graceful shutdown, etc.