Rendering Strategies
2026-08-04 · 12 min read
What is a rendering strategy?
A rendering strategy is a technique used to generate and deliver content to users in a web application. There are several rendering strategies available to use, each with their own use cases and tradeoffs: Client-Side Rendering, Server-Side Rendering, Static Site Generation, Incremental Static Regeneration, Streaming SSR, and React Server Components.Choosing the right rendering strategy for your application is crucial to ensure that it meets the needs of your users and business goals. Below we'll explore the different rendering strategies and when to use them.
Client-Side Rendering (CSR)
A rendering strategy where the browser downloads a minimal HTML page and uses JavaScript to render the content on the client side.
This approach is commonly used in Single Page Applications (SPAs) where the initial page load is fast, and subsequent navigation is handled by JavaScript without requiring a full page reload.
CSR is suitable for applications that require rich interactivity and dynamic content, such as drawing tools, and dashboards. However, CSR can have drawbacks in terms of SEO and initial load time, as search engines may have difficulty indexing JavaScript-rendered content, and users may experience a delay in seeing the content while the JavaScript is being downloaded and executed, i.e. the cost is that the first paint depends entirely on how fast the client can load and execute the JavaScript bundle.
When to use CSR
- Page lives behind authentication or is not indexed by search engines
- User opens the page and interacts with it for a long time, e.g. a dashboard
- The page is highly interactive and requires frequent updates
- App needs offline support via service workers
When not to use CSR
- The page needs to be indexed by search engines
- The page needs to load quickly for users or your user base is on slow networks/devices
- LCP and TTFB are critical business metrics
Metrics
| Metric | Outcome |
|---|---|
| LCP | Not Great |
| TTFB | Fast (server returns a minimal HTML page) |
| SEO | Not Great |
| Server cost | Low |
| Hydration cost | High |
If you do end up using CSR, there are some optimizations you can make to improve the user experience.
- Code splitting: split your JavaScript bundle into smaller chunks that can be loaded on demand, reducing the initial load time.
- Suspense and lazy loading: use React's Suspense and lazy loading to load components only when they are needed, reducing the initial load time. Combine with skeleton loaders to improve perceived performance and layout shift.
- Defer non-critical UI/JS: defer loading of non-critical UI/JS until after the initial render, improving the time to first paint.
- Preload critical resources: use the
<link rel="modulepreload">for entry chunks and<link rel="preload" as="fetch">tag to preload first data request and improve the time to first paint.
Server-Side Rendering (SSR)
A rendering strategy where the server generates the HTML for a page on each request and sends it to the client, where the client hydrates it.
The browser downloads the HTML and renders it, allowing for faster initial page loads and better SEO, before any JS is executed. Once the document is loaded, we hydrate the page and attach event listeners to the HTML to make it interactive.
Hydration is the step that walks the server-rendered DOM and attaches event listeners. Because it has to run the entire component tree at least once, it can be slow and cost roughly the same amount of time as client render. This creates a period where the content looks ready (painted) but is not interactive (hydrated). There are ways to improve this using the following patterns:
- Streaming SSR + Suspense: hydrate parts of the page as they are streamed from the server, instead of blocking the entire page until all components are hydrated.
- Selective Hydration: prioritize hydrating whichever the component the user is currently interacting with, and defer the rest. This can be done using React's
useTransitionhook to prioritize updates to the component tree. - React Server Components: RSC eliminates hydration for any component that doesn't need to be interactive since their JS never ships at all.
When to use SSR
- The page needs to be indexed by search engines
- Conversion-sensitive first paint
When not to use SSR
- 100% static pages: use static rendering instead
- Highly interactive applications: use CSR instead because hydration cost outweighs the benefits of SSR
Metrics
| Metric | Outcome |
|---|---|
| LCP | Depends on the server response time and size of payload |
| TTFB | Slow (per-request) |
| SEO | Excellent |
| Personalization | Personalized data available at request time |
| Server cost | High, server regenerates on every request |
| Hydration cost | High |
Static Site Generation (SSG)
A rendering strategy where the HTML for a page is generated at build time and served as static files to the client.
This approach is commonly used for content-heavy websites, such as blogs and documentation sites, where the content does not change frequently. Each URL maps to a static HTML file that can be served quickly to the client combined with a CDN, allowing for fast page loads and improved SEO. However, SSG can have drawbacks in terms of content freshness, as any changes to the content require a rebuild of the entire site.
When to use SSG
- Content does not change frequently and can be generated at build time
- The page needs to be indexed by search engines
- Low cost of serving static files, as they can be cached and served from a CDN
When not to use SSG
- Content changes per request or frequently, requiring a rebuild of the entire site
- The page needs to be personalized for each user, requiring dynamic content generation or testing different variations of the page for A/B testing
Metrics
| Metric | Outcome |
|---|---|
| LCP | Excellent |
| TTFB | Excellent, especially when combined with CDN |
| SEO | Excellent |
| Personalization | None |
| Server cost | Low |
| Cache Invalidation | Rebuild + Redeploy |
As mentioned, as content changes and grows, SSG can become expensive and slow to rebuild and redeploy. Imagine an e-commerce site with thousands of products and each page takes several ms to build. To address long build times, we can
- Prerender top-N pages and use Incremental Static Regeneration (ISR) to update the rest without rebuilding the entire site.
- Parallelize builds to reduce build time.
- Cache pages between builds so you can serve the last known good version of a page while the new version is being built.
- Use on-demand revalidation to rebuild only the pages that have changed.
Incremental Static Regeneration (ISR)
A rendering strategy that allows you to update static pages after the initial build without rebuilding the entire site.
This approach is commonly used for content-heavy websites, such as blogs and e-commerce sites, where the content changes frequently but not on every request. With ISR, you can specify a revalidation time or "on-demand hook" for each page, and the server will regenerate the page in the background when a request comes in after the revalidation time has passed. This allows for faster page loads and improved SEO, while still allowing for content freshness.
- Time-based: prerender a page, then refresh it at most N seconds. The cached HTML will be served until the next request after N seconds, at which point the page will be regenerated in the background and the new version will be served to subsequent requests.
- On-demand: instead of refreshing at a fixed interval, you can trigger a rebuild of a page or group of pages when content changes.
Another technique is to use a hybrid approach, where you prerender the most popular pages and use ISR for the rest. This allows you to take advantage of the benefits of SSG for the most important pages, while still allowing for content freshness on less popular pages.
ISR is most powerful on the edge, where you can use a CDN to cache the static pages and serve them quickly to users, while still allowing for content freshness through ISR. One thing to note is that revalidation doesn't happen immediately across every edge, if you need byte-for-byte global consistency, purge the CDN cache directly.
| Metric | Outcome |
|---|---|
| LCP | Excellent |
| TTFB | Excellent, especially when combined with CDN |
| SEO | Excellent |
| Personalization | None |
| Server cost | Low |
| Cache Invalidation | Until next revalidate signal |
Streaming SSR
The server sends minimal HTML down and streams in deferred content as chunks resolve.
In vanilla SSR, the entire page is rendered on the server (including every async data fetch the page needs) and sent down to the client as HTML; if the slowest query takes 1000ms, then the page's fastest parts have to wait at least 1000ms for any content to paint.
The modern answer to this is streaming SSR (React 18 + Suspense) where the server sends HTML in chunks as each piece resolves, so fast parts paint immediately while slow parts stream in later. Next.js App Router uses this by default; wrapping a slow component in <Suspense> lets everything else render and ship without waiting for it.
Hydration with Streaming In streaming SSR, hydration is both selective and progressive rather than all-at-once like in vanilla SSR. How hydration happens:
- React's runtime JS loads on the client as HTML is still streaming in
- React hydrates already-arrived chunks immediately (it doesn't wait for the full page)
- When a slow chunk finally resolves on the server, it streams down as HTML plus a small inline
<script>that tells React where in the DOM to swap the fallback for the real content - React hydrates just that new chunk
- If a user interacts with a not-yet-hydrated component, then React will prioritize hydrating that component first; this is known as selective hydration.
With streaming SSR, you have to think about which components to wrap in
<Suspense>and what fallbacks to show, and there are edge cases around hydration mismatches if server/client state diverges mid-stream. Fallbacks are not optional with streaming so make sure to wrap your<Suspense>wrappers with<ErrorBoundary>.
There are tradeoffs with using streaming SSR:
- You can't change response headers after the first chunk. Components that error and would have returned an error code, will be locked as 200. Render an error message within your fallback.
- Suspense changes data fetching patterns: components that suspend must use a Suspense-aware data layer like the
use()hook or Next.jsfetch,useEffect()doesn't work. - Caching is harder
- Suspense waterfalls can happen: nested
<Suspense>boundaries resolve sequentially so outer<Suspense>must resolve before the inner one starts fetching.
The best use case for streaming SSR is when different parts of a page have meaningfully different latencies and can be rendered independently. If everything on the page depends on one slow query, streaming doesn't help much. Some common use cases are:
- Data-heavy dashboards: each chart has its own query and can stream in as they load.
- News sites with personalization: article body is static and can stream in instantly and personalized sections can be streamed in later.
React Server Components
React components that run exclusively on the server, never ship their code to the browser, and produce output that client tree can render and interleave with normal client components.
In traditional React, every component's code ships to the browser, even components that just fetch data and render static HTML. RSC lets you write components that stay entirely on the server. Their output (rendered HTML) is sent to the client, but the component code itself never is.
What that means practically:
// This component runs on the server only.
// The `db` import never ships to the browser.
import db from './db'
export default async function UserProfile({ id }) {
const user = await db.users.findById(id) // direct DB access, no API needed
return <div>{user.name}</div>
}
You can await directly in the component body, import server-only libraries (database clients, secrets, heavy parsing libraries), and access the filesystem — none of it touches the client bundle.
How it's different from SSR SSR renders components to HTML on the server for the initial page load, but then ships all the component code to the client for hydration. RSC components never hydrate, they have no client-side lifecycle at all; they render once on the server and that's it.
The component tree for RSC is mixed. A typical app will have both server and client components in the same tree. Client components are marked with use client; and work exactly like traditional React components; they ship to the client, hydrate, and can use state and effects. Server components cannot use state or effects and are essentially used as async functions that fetch data and return markup and handoff interactivity to client components down the tree.
The real wins:
- Zero bundle cost for server-only code
- Direct backend access without an intermediate API layer
- Sensitive logic (API keys, business rules) never leaves the server
- Smaller client bundles mean faster hydration