SSR And Prerendering · How it works

2 min read
Senior4 min read
Rapid overview

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.

See also