Rendering strategy is often presented as a framework choice: pick a checkbox when creating an application, then defend it forever. That framing hides the real decision. SSR, CSR, SSG, and ISR describe when HTML is produced, where data is read, and how long the result may be reused. A product can use all four, sometimes on one route.

The useful question is not “Which acronym is fastest?” A cached static document can have excellent time to first byte but show stale inventory. A server-rendered account page can deliver meaningful HTML quickly but spend too much compute per request. A client-rendered dashboard can personalize naturally but leave users staring at an empty shell while JavaScript and data cross the network. Each strategy moves latency, freshness, complexity, and failure to a different part of the system.

This article builds a route-level decision model. It follows the bytes from data source to HTML, then from HTML to an interactive page, and connects those timings to SEO, Core Web Vitals, caching, personalization, and operating cost.

Start with two timelines: HTML and data

A browser cannot render application content until it receives enough HTML or JavaScript to create it. It cannot make that content interactive until the relevant code has loaded and event handlers are attached. These are separate milestones:

  • HTML availability: when meaningful elements exist in the response or DOM.
  • Data availability: when the values needed by those elements are known.
  • Visual availability: when the browser has styled, laid out, and painted them.
  • Interactivity: when user actions can execute the intended application logic.

In CSR, the first response commonly contains a root element and script tags. Data arrives after JavaScript runs. In SSR, the server reads data and emits meaningful HTML during the request. In SSG, that read and render happen during a build. ISR begins with a generated document but permits regeneration after a freshness window.

sequenceDiagram participant B as Browser participant E as CDN or edge participant A as App server participant D as Data source B->>E: GET /products/42 alt Fresh static or ISR cache entry E-->>B: Cached HTML else SSR or regeneration E->>A: Forward request A->>D: Read route data D-->>A: Product data A-->>E: Rendered HTML plus serialized state E-->>B: HTML response end B->>E: Request JavaScript chunks E-->>B: JavaScript B->>B: Hydrate interactive regions

Hydration is the bridge between server-produced HTML and a client application. The client framework recreates the component tree, associates it with existing DOM nodes, and installs behavior. Hydration is not free: code must be downloaded, parsed, and executed even though the pixels are already visible. A page may therefore have a good Largest Contentful Paint (LCP) but a poor Interaction to Next Paint (INP) if a large hydration task blocks the main thread.

Note

Server rendering does not eliminate client JavaScript. It changes what users and crawlers receive before JavaScript runs. Islands, partial hydration, and server-only components can reduce the amount that must hydrate, but plain SSR usually sends both HTML and application code.

CSR puts rendering and personalization in the browser

Client-side rendering serves a reusable shell and lets the browser fetch data and construct the view. It fits authenticated tools where most content is user-specific, sessions are already required, and search indexing is irrelevant: editors, analytics consoles, inboxes, and internal operations screens.

A Vue route can render an honest loading state and cancel obsolete work when its input changes:

vue
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue';
import { getProject, type Project } from './project-api.js';

const props = defineProps<{ projectId: string }>();
const project = ref<Project>();
const error = ref<string>();
let controller: AbortController | undefined;

watch(
  () => props.projectId,
  async (projectId) => {
    controller?.abort();
    const requestController = new AbortController();
    controller = requestController;
    project.value = undefined;
    error.value = undefined;

    try {
      project.value = await getProject(projectId, requestController.signal);
    } catch (cause) {
      if (!requestController.signal.aborted) {
        error.value = cause instanceof Error ? cause.message : 'Request failed';
      }
    }
  },
  { immediate: true },
);

onBeforeUnmount(() => controller?.abort());
</script>

<template>
  <p v-if="error" role="alert">{{ error }}</p>
  <ProjectSkeleton v-else-if="!project" />
  <ProjectWorkspace v-else :project="project" />
</template>

CSR reduces HTML generation load and makes CDN caching of the shell straightforward. Navigation after startup can feel immediate because the runtime persists and fetches only data. It also creates a dependency chain for a cold visit: HTML, JavaScript, execution, API request, then render. Large bundles, a slow device, an API failure, blocked scripts, or a runtime exception can leave no useful content. Loading placeholders should reserve stable dimensions so late data does not cause Cumulative Layout Shift (CLS).

Search engines can execute JavaScript, but relying on that capability adds uncertainty and delays discovery. Link previews, simple crawlers, accessibility tooling, and users on constrained devices benefit from meaningful source HTML. CSR is therefore a poor default for public articles and product pages whose content is the reason for the visit. It remains excellent for interaction-heavy private surfaces when paired with route-level code splitting, request cancellation, useful error states, and measured bundle budgets.

SSR computes a response for each request

Server-side rendering reads request-time context, loads data, and produces HTML before returning the response. It naturally supports content that must be fresh or personalized on the first paint: a localized storefront with current prices, a search result affected by location, or an account page with permission-dependent navigation.

SSR improves the chance of a strong LCP because the browser can paint content without waiting for application code to fetch it. It also gives crawlers complete titles, headings, links, structured data, and canonical metadata. The cost appears in TTFB and server capacity. Before the first byte can leave, the server may need to authenticate, call several services, and render a component tree. One slow dependency becomes every visitor’s slow response.

Streaming SSR relaxes that barrier. The server can send the document shell and completed regions while slower boundaries continue rendering. Streaming can improve perceived progress and avoid waiting for the slowest query, but it does not make that query faster. Status codes and headers may become committed before a late failure occurs, and crawlers or caches must receive a coherent final document.

Hydration correctness is another boundary. The client must initially render the same structure as the server. Reading window, the current clock, random values, or browser-only storage during initial render can cause mismatches. Serialize the exact request data safely and use it as the client’s initial state:

ts
import { createSSRApp, type Component } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { escapeJsonForHtml } from './html.js';
import { loadProduct } from './products.js';

export async function renderProductPage(
  App: Component,
  productId: string,
): Promise<string> {
  const product = await loadProduct(productId);
  const state = { product };
  const app = createSSRApp(App, state);
  const body = await renderToString(app);

  return `<!doctype html>
<html lang="en">
  <body>
    <div id="app">${body}</div>
    <script>window.__STATE__=${escapeJsonForHtml(state)}</script>
    <script type="module" src="/assets/client.js"></script>
  </body>
</html>`;
}

escapeJsonForHtml must neutralize characters that could terminate a script element; plain JSON.stringify inserted into HTML is not a complete XSS defense. The browser entry should pass the same state to createSSRApp before mounting. Avoid shipping secrets or server-only fields in serialized state.

SSR failure modes include dependency fan-out, exhausted database pools, render exceptions, memory pressure, and retry storms from upstream proxies. Bound request deadlines, parallelize independent reads, cache shared data, limit concurrency, and return a deliberate degraded page when a nonessential service fails. Measure p95 and p99 render time, not only averages.

SSG and ISR move work away from the request path

Static site generation resolves data and produces HTML during a deployment build. The resulting files can be served from object storage or a CDN without application compute on a cache hit. That gives public documentation, articles, campaign pages, and stable catalogs low TTFB, broad resilience, predictable SEO, and a small attack surface.

flowchart LR C[Content or database] --> B[Build process] T[Templates and components] --> B B --> H[Versioned HTML and assets] H --> O[Object storage] O --> E[CDN cache] E --> U[Browser] W[Publish webhook] --> Q[Build or revalidation queue] Q --> B

The tradeoff is freshness. A correction is invisible until a successful build and deployment reach the serving layer. Build duration grows with route count and data fan-out. A full build of a million product pages may be slower and more expensive than rendering the small active subset on demand. Build failures can block unrelated updates, while deleted routes require explicit cleanup or redirects.

Incremental Static Regeneration keeps the static serving model while refreshing individual routes. Implementations vary, but a common stale-while-revalidate policy works like this: a cached page is fresh for a configured interval; after expiry, one request triggers regeneration while visitors continue receiving the stale document; a successful render atomically replaces the entry. Some systems instead block the first request or regenerate from a publishing event.

That policy needs precise answers. Is the interval a maximum age or merely the earliest revalidation time? What happens if regeneration fails? Is there one cache per region? How are related pages invalidated after a price or navigation change? Without request coalescing, a popular expired route can cause a thundering herd. Without versioning and atomic replacement, users may receive partial output.

Warning

Never use a shared HTML cache for responses containing user-specific or permission-protected data unless the cache key and Vary behavior fully partition every relevant dimension. The safest default is to keep private HTML uncacheable and fetch personalized fragments separately.

Compare performance, SEO, freshness, and cost

No strategy owns a metric. Architecture changes the critical path; implementation determines the result. A bloated SSG page can have poor LCP and INP. Lean streaming SSR can outperform it for a dynamic route. Use field data segmented by route, device, geography, and cache status.

Strategy First meaningful HTML Typical freshness SEO baseline Personalization Main operating cost
CSR After JS execution and data fetch Request-time API data Weak without prerendering Natural in browser API capacity and client bundle delivery
SSR During each request Request-time Strong Strong, but limits shared caching Render compute, dependencies, observability
SSG During build Deployment-time Strong Usually client-side fragments Builds, storage, cache invalidation
ISR Build or background regeneration Bounded or event-driven staleness Strong Shared page plus dynamic fragments Regeneration, distributed cache coordination

TTFB measures how quickly response bytes begin. CDN-served SSG and ISR usually excel; SSR includes routing, data, and render work; CSR can have a fast shell TTFB that says nothing about content readiness. LCP measures when the largest visible content paints. Early HTML helps, but image priority, CSS, fonts, server latency, and client rendering all matter. INP reflects interaction latency across the visit. Excessive hydration, long tasks, and third-party scripts hurt it regardless of where HTML originated.

SEO requires more than rendered body text. Each indexable URL needs stable status codes, canonical URLs, language alternates, metadata, structured data, and crawlable links. Do not return 200 for missing SSR records or generate thousands of low-value parameter combinations. SSG can detect broken links during a build; SSR must enforce the same invariants at runtime.

Personalization and cacheability pull in opposite directions. Instead of rendering an entire homepage per user, cache a shared editorial page and load a small signed-in recommendation region after authentication. Edge-side includes or composable CDN features can assemble fragments, but they add cache keys and failure modes. Often the simplest hybrid is public HTML plus client-side private islands.

Choose per route and operate the hybrid deliberately

Start with the route’s invariants, not the framework default. Ask how stale the content may be, whether anonymous users share it, whether first-visit content must be indexed, what data is needed above the fold, and what should remain usable when JavaScript or an origin service fails.

Route Sensible default Reason
Documentation article SSG Public, highly cacheable, changes on publish
News story SSG or event-driven ISR Read-heavy with explicit publication events
Product detail ISR plus live price/stock fragment Shared description, volatile commercial data
Search results SSR or CSR by SEO policy Query-dependent and potentially expensive
Account settings SSR shell or CSR Private, personalized, not indexable
Collaborative editor CSR after an SSR/SSG entry shell Interaction and live state dominate

A hybrid product might statically generate category pages, incrementally regenerate product descriptions, server-render the cart, and client-render the checkout’s address selector. Even one route can use a static frame, an SSR availability summary, and a hydrated purchase control. Keep ownership visible: document the cache key, freshness target, invalidation trigger, fallback, and responsible service for every rendered region.

Operationally, observe cache hit ratio, regeneration duration and failures, origin render latency, build queue age, deployment time, hydration errors, JavaScript bytes, and route-level Web Vitals. Tag SSR traces with cache status and dependency timings. Test cold caches and slow origins, not just warm local navigation. Preserve the last known good static artifact when a build fails, and serve stale content within an explicit limit when regeneration fails. For security or price corrections, provide a purge path that bypasses the normal freshness window.

Cost follows work placement. SSG pays in build compute and storage; SSR pays on traffic; CSR shifts rendering to user devices but still pays for APIs and larger script delivery; ISR pays in cache infrastructure and invalidation complexity. Compare cost per successful visit, including observability, incidents, and developer time. The cheapest request path can be the most expensive system to reason about.

Takeaways

  • Rendering strategy is a scheduling and caching decision: decide when HTML and data become available, where work runs, and how long output is reusable.
  • CSR suits private, interaction-heavy applications, but its cold path depends on JavaScript execution and a later data request.
  • SSR provides request-time HTML and strong personalization, while adding server latency, hydration constraints, and per-request failure modes.
  • SSG removes origins from the hot path; ISR adds controlled freshness but requires regeneration locking, atomic replacement, invalidation, and stale policy.
  • TTFB, LCP, and INP describe different phases. Optimize and measure all three with cache status and route context.
  • SEO and resilience improve when meaningful HTML exists without JavaScript, but correct metadata, status codes, links, and fallbacks still require explicit work.
  • Choose at route or component granularity. A documented hybrid is usually more honest and efficient than forcing every page into one acronym.