If you've spent any time choosing between Single-Page Apps (SPA), Server-Side Rendering (SSR), Static Site Generation (SSG), and React Server Components (RSC), you've probably noticed they feel like four disconnected things you have to memorize independently. Each comes with its own vocabulary, its own trade-offs table, and its own set of opinions about when it's "the right choice." The whole landscape can feel disorienting, especially when every article frames the decision differently.
Here's the thesis: every React rendering strategy is an answer to a single question: when does the data fetch happen?
Under that lens, the four patterns collapse into a clean spectrum ordered by data-fetch timing. The downstream choices everyone argues about, like bundle size, First Contentful Paint, and SEO, become predictable consequences of that one decision, not independent variables you need to evaluate separately.
By the end of this piece, you'll have a framework for choosing a rendering strategy that starts with reasoning about your data, not about where HTML gets generated.
In brief
- SPA, SSR, SSG, and RSC make more sense when you compare them by when data is fetched.
- Bundle size, First Contentful Paint, and SEO usually follow from that data-timing choice.
- Most real applications mix multiple data timings on the same page instead of choosing one strategy everywhere.
- A practical decision framework starts with freshness, audience, and where the compute should happen.
Why React Rendering Strategies Feel Disconnected
Most rendering content leads with HTML delivery: where markup is generated, how it travels over the network, when it hydrates into an interactive app. That framing isn't wrong, but it makes SPA, SSR, SSG, and RSC look like four separate categories, each with its own quirks to memorize.
The confusion runs deep. Developers routinely conflate RSC with SSR, reduce SSR to "the SEO strategy," or assume client components are deprecated now that server components exist.
The State of JavaScript indicates that single-page apps remain one of the most common application patterns, alongside server-side rendering. The 2024 State of JavaScript survey reported 14,015 total respondents. Meta frameworks showed differing levels of adoption. For most of us, server-centric rendering is new territory being encountered from the outside.
The HTML-delivery framing is one layer up from the actual axis of differentiation. Underneath every pattern is a single decision about when the data fetch happens. That timing decision determines where rendering runs. In turn, that determines everything developers argue about: how big the bundle is, how fast the first paint fires, and whether search crawlers can see the content.
Once you see rendering strategies as points on a data-timing spectrum, choosing between them stops being a memorization exercise and starts being a straightforward question about how your data actually behaves.
Sign up for the Logbook, Strapi's Monthly newsletter
The Four Moments When React Can Fetch Data
Each rendering strategy locks in a specific moment when the data fetch actually runs. Walk through them from earliest to latest, and the spectrum becomes clear.
1. Build Time: Static Site Generation (SSG)
Data is fetched once during the build process, in your CI/CD pipeline, before any user ever makes a request. The fetched data gets baked into static HTML files and deployed to a CDN. Every visitor receives the same pre-built content until the next build runs.
The Next.js docs describe the model this way: "With static rendering, data fetching and rendering happens on the server at build time (when you deploy) or when revalidating data. Whenever a user visits your application, the cached result is served."
The cost profile is compelling: zero server compute per user request after the initial build. The trade-off is freshness. Content is frozen until the next deploy or revalidation cycle.
2. Request Time on the Server: Server-Side Rendering (SSR)
Data is fetched per request, on the server, before any HTML leaves for the browser. The server runs React, embeds the fetched data into fully rendered HTML, and sends the complete response.
Fresh data on every visit, but with a real cost: the server runs the data fetch and the React render for every single request. CDN caching for SSR responses ultimately works similarly to SSG: the CDN caches rendered HTML and serves it to subsequent visitors, with the main difference being that SSR pages are generated on demand at the origin before being cached, whereas SSG pages are pre-built at build time.
Here's an architectural detail that trips people up in the traditional Next.js Pages Router model. Data fetching with getServerSideProps only works at the page level, at the top of each route. You can't pop a getServerSideProps function anywhere you want. Components deeper in the tree are passive recipients of data passed down as props.
3. After the Client Bundle Loads: Single-Page Apps (SPA)
The server delivers an HTML shell (essentially a <div id="root"></div> and some <script> tags). The browser downloads the JavaScript bundle, parses and executes it, mounts React, and only then triggers data fetches via useEffect or libraries like React Query.
In this model, the client receives HTML, downloads JavaScript, React boots and renders a layout shell, but at that point, there is no actual data. The data fetch is a second network round-trip that can only begin after JavaScript has loaded.
If that sounds slow, it is. Sequential network operations stand between the user and meaningful content. That's the fundamental cost of client-side data timing.
4. Per Component, on the Server: React Server Components (RSC)
Each server component is an async function that directly awaits its own data: a database query, an API call, a filesystem read.
// A server component fetches its own data directly
async function ProductDetails({ id }) {
const product = await db.query('SELECT * FROM products WHERE id = $1', [id]);
return (
<div>
<h2>{product.name}</h2>
<p>{product.description}</p>
</div>
);
}The component runs entirely on the server. Neither the function body nor the database driver reaches the client bundle.
React docs specifies: "Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server."
The defining distinction from traditional SSR is granularity. Any server component at any depth in the tree can independently fetch its data. This is co-located, per-component data fetching rather than the centralized top-of-route fetch that traditional SSR requires. Dan Abramov framed it directly: "Server Components is not really related to SSR. I like to think of Server Components as componentized BFF ('backend for frontend') layer."
Because the fetches happen server-side, the round-trip latency between fetch and data source is minimal: server to database rather than client to server to database.
Why Bundle Size, FCP, and SEO Follow From Data Timing
Once you've locked in when data is fetched, the performance metrics everyone obsesses over stop being independent variables. They become predictable consequences of the data-timing decision you already made.
1. Bundle Size Tracks Where Rendering Happens
Client-side rendering means shipping all rendering logic to every visitor's device: the components, the data-fetching code, the libraries that transform data into UI. The bundle has to contain everything because the browser is doing all the work.
SSR and SSG don't actually reduce this. As web.dev explains, JavaScript still needs to be fetched for interactivity, typically through hydration. Hydration typically requires downloading JavaScript for the components that need to become interactive, though unlike a full SPA this code can often be loaded progressively, partially, or deferred.
RSC is one strategy that can causally reduce bundle size. Server component code is excluded from your JavaScript bundle by definition. The components run on the server, and only their rendered output crosses the network. A server component using heavy libraries like marked and sanitize-html sends neither library to the browser, only the rendered HTML. The bundle size debate isn't really about minification or tree-shaking. It's about which side of the network is doing the rendering.
2. First Contentful Paint Tracks When Data Lands in HTML
FCP measures the time from navigation to the first piece of content rendered on screen. The critical branch point is simple: is content-bearing HTML present in the initial HTTP response, or does it need to be constructed after JavaScript executes on the client?
SSG and SSR put data in HTML before the browser ever sees it. For SSR/SSG pages, FCP is often largely bounded by Time to First Byte (TTFB) plus HTML parse time, though render-blocking JavaScript can still delay it. SSG goes further because data was fetched at build time, so there's no per-request server compute delay, and the pre-built HTML arrives from a CDN edge.
SPA delays FCP through the entire chain: HTML shell arrives, JavaScript downloads, JavaScript executes, client-side fetch fires, data returns, components finally render. As web.dev notes, client-side rendering can delay when content is first displayed because the app may need to load and execute JavaScript before anything can be shown.
Next.js streaming adds a nuance for RSC. The shell streams immediately, and data-dependent chunks stream in progressively as their fetches complete. FCP fires fast on the shell content. Slower data doesn't block the initial paint.
The FCP gap between strategies is just the gap between data-arrival times.
3. SEO Tracks Whether Crawlers See Content on First Response
Googlebot processes pages in three stages: crawling (fetching the raw HTML), rendering (executing JavaScript in a queued headless browser), and indexing. Content present in the initial HTML is available at crawl time. Content generated by JavaScript requires the render queue, and render queue timing may take a few seconds or longer.
SSG and SSR put content in the first HTTP response. Googlebot can extract and index it immediately, no JavaScript execution required. SPA delivers an empty shell; the content is only discoverable after the render queue processes the page.
Here's the non-obvious part. An SSR application that renders a shell on the server but fetches its primary content client-side after hydration provides no indexing advantage over a pure SPA for that content. SEO reliability isn't an SSR feature. It's a side effect of fetching data before the response leaves the server, regardless of which strategy achieves that.
Mixing Data Timings on a Single Page
Most production apps don't pick one data-timing for the whole application. They pick a different timing for each piece of content, based on how that content actually behaves.
Different Content on the Same Page Has Different Freshness Needs
A page is rarely uniform. Some sections, like titles, descriptions, and structural copy, change every few days or weeks. Others, like inventory counts, personalized pricing, and live activity feeds, change constantly or depend on who's asking.
Forcing all of it through one rendering strategy means either re-fetching static content on every request, which wastes server compute, or serving stale data where freshness matters, which frustrates users. Next.js documentation distinguishes between static and dynamic rendering approaches.
The practical move is to assign each section the data-timing that matches its actual freshness profile. The App Router model enables this by default: layouts and pages are server components, and you layer in client components where you need interactivity or browser APIs. The unit of decision is the component, not the route.
A Practical Example: The E-Commerce Product Page
Consider a product page, one of the most common mixed-rendering scenarios. A product page can mix cached server-rendered sections, periodically revalidated product data, personalized request-time content, and client-side interactive elements like add-to-cart functionality.
Here's how the data-timing breaks down:
- Title, description, base price, and product images come from a slow-changing source. This data is the same for every visitor and changes only when the product team updates it. Build-time fetching with periodic revalidation fits ISR territory.
- Live inventory at the user's nearest warehouse and loyalty-discounted pricing depend on who's asking. These require request-time data that incorporates user session information. Client-side fetching, or a dynamic server component that reads cookies, handles this naturally.
- A recommendations carousel can be a server component that fetches personalized picks at render time. The recommendation logic, the API client, and the data transformation stay on the server instead of shipping to the browser. With Suspense wrapping, the recommendations can stream in after the static shell without blocking the rest of the page.
Same page, three different data-timings, each chosen to match the data's real behavior. The framework handles the composition. You handle the data reasoning.
A Data-First Framework for Choosing a Rendering Strategy
The reframe pays off when you make decisions with it. Three questions, asked in order, point to the right pattern for any given piece of content.
1. Ask How Often the Data Changes
This is the first filter, and it narrows the field substantially.
Slow-changing data, like blog posts, product descriptions, and documentation, fits build-time generation. Fetch it once, cache the result on a CDN, and revalidate on a schedule or in response to a content update event using on-demand revalidation.
Data that changes per request, like search results, anything involving cookies() or headers(), and content that depends on URL parameters, needs request-time rendering. The server fetches fresh data for every visitor.
Real-time session data, like form state, filters, live updates, and other interaction-driven state, is typically handled on the client side rather than fetched as prerendered server data. The data lifecycle is tied to the browser session, not to a server request.
2. Ask Who the Data Is For
Public data that's identical for every visitor points to static or ISR rendering. As the Next.js documentation notes, "Static rendering is useful for UI with no data or data that is shared across users, such as a static blog post or a product page." This data can be cached aggressively at the CDN edge.
Personalized data, whether scoped to a user session, dependent on authentication state, or derived from client-side context, can't be cached at the edge without per-user cache segmentation. This points toward dynamic server rendering, client-side fetching, or per-component server fetches that read session data.
Most real pages contain both. In Next.js App Router, dynamic routes can still use cached data alongside uncached data, because data caching is handled separately from full-route caching of HTML and the RSC payload.
3. Ask Where the Work Should Happen
Server-side rendering moves compute cost from the visitor's device to your infrastructure. Client-side rendering does the opposite. The choice is real and has budget implications.
A quick way to think about it:
- Server compute scales with traffic: more requests, more CPU time, more cost.
- Client compute scales with device variance: an app that feels fine on a fast laptop may struggle on a mid-range phone on a 3G connection.
- Server components fit when data fetching requires secrets, when you want to avoid client-server fetch waterfalls, or when rendered output is cacheable across requests.
- Client components fit when data changes in response to user interaction, when you need optimistic UI updates, or when browser-only APIs like
localStorageor geolocation are involved.
Consider both your infrastructure costs and the device profile of your actual users.
Picking the Right Pattern Starts With the Right Question
Most rendering debates pit framework against framework, or pattern against pattern. Both framings hide the actual decision.
The rendering strategy question is, at its core, a data question. How often does this content change? Who is it for? Where should the compute happen? Once you answer those for each piece of your page, the rendering strategy is almost determined for you. SSG, SSR, SPA, and RSC stop being four disconnected patterns to memorize and become related approaches distinguished by when and where rendering happens, and how data is fetched.
The framework choice becomes a secondary question: which tool best supports the data-timings you already picked? That's a much easier decision to make, because you're evaluating tools against concrete requirements instead of comparing abstract trade-off tables.
Good rendering decisions start with reasoning about data, not about HTML.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.