SSR, SSG, ISR, PPR—these are acronyms that might initially seem like a confusing and intimidating alphabet soup. However, understanding them doesn’t have to be a daunting task. Each of these terms represents a different strategy or methodology, and with a little bit of explanation, they can become much clearer and less frightening.

This post aims to demystify these concepts, providing a clear and comprehensive guide to help developers choose the right strategy for their projects.

Let’s go!

Jump to headingStatic

Early websites on the web were simple and static text documents, which were served from a basic web server architecture where every person would see the same copy of the page served from a HTTP request.

Static pages (or static rendering) can have many variations to serve different purposes, from plain HTML files to on-demand generated pages as we’ll see later in this post.

server-client diagram

In this model, the server receives a request from the client, retrieves a copy of the requested page, and replies with a response containing the page content, status code, headers, and resources, such as images, icons, and fonts.

One of the most important benefits of static pages is that can be easily cached and delivered by fast CDNs to provide resilient and low-latency content to your end-users. You can think of a CDN as a distributed network of computers in charge of delivering content next to the user’s geolocation.

What is the difference between a CDN and Edge?

Both terms can sound similiar as both CDN and Edge can run globally and deal with high volumes of traffic, but generally speaking when we say something is “at the edge” or deployed at the edge we talking about the architecture pattern of edge computing.

In edge computing, we can run long tasks/functions in a distributed network close to the user to provide personalized experiences quickly.


AWS, Netlify and Vercel cloud providers are some examples.

Jump to headingSSG

SSG stands for Static Site Generation and is a rendering pattern used to pre-render web pages at build time.

Ok, let’s break those fancy terms down:

  • pre-render :
    • Pre-rendering refers to the process of generating HTML pages ahead of time before they are ever sent to the users. It usually happens during the build phase of your app.
  • build time :
    • Build time is when you’re compiling the artifacts of your web application. During this phase, you can perform tasks such as data fetching, transforming, or bundling files.
    • This step is usually triggered by a npm run build command.

When we say to pre-render it means that by the time a request comes, we only need to serve the pre-baked page generating everything again and again.

diagram explaning the ssg build process

The nicest thing is you can run any arbitrary code when generating those pages, including performing database queries, getting content from a CMS, calling external resources, etc.

By default the number of generated HTML pages generated at build time will depend on how many pages you have stored on your app, it can be the products you’re listing, posts you’ve written, etc. Usually, the framework you’re using will give you the option to dictate which pages need to be generated ahead of time, for example, the generateStaticParams (or getStaticPaths) in Next.js.

The SSG pattern became the preferred choice for content-based apps such as Blogs, Docs, or Landing pages due to its simplicity, solid performance, and security. Being able to achieve good metrics in web vitals, most notably FCP and TTI.

It is also one of my favorites ✨

11ty, Jekyll, Hugo and Astro are some examples or static site generators.

Jump to headingDynamic

“A dynamic website is one where some of the response content is generated dynamically, only when needed.” - MDN Docs

Essentially, dynamic pages are generated for each time they are requested. Dynamic sites can return highly personalized content based on user preferences and data.

The code you write to support dynamic websites must run on servers, mostly known as “server-side rendering” and it is very popular amongst frameworks like Ruby on Rails, Laravel, .NET, and Django.

Jump to headingSSR

SSR or Server-side rendering is the technique used to dynamically render a full page on every request on the server. You can use it to perform backend operations like data-fetching, querying a Database, and templating based on the HTTP request received and its headers, cookies or query params associated.

It’s a suitable pattern for building recommendation feeds, social media, or authentication-based pages.

The major difference between SSR and Client-side Rendering (or CSR) is that running code on the server can achieve better performance and SEO as we don’t need don’t rely solely on JS to be parsed and executed before the user can see the page. Some notable performance metrics that achieve good values by SSR are FCP as well as LCP.

diagram explaning the ssr request-response cycle

It is important to note that although I have the power to run code server-side, I can’t be careless about unoptimized or insecure code that could lead to errors and delay the response of our users too much.

What if I need my dynamic page also have interactivity on the client-side?

To be able to render our interactive UI based on server code, in the end, we need some JavaScript to mount the components in the generated HTML file.

For example, in React this can be done by using the renderToString method.

// server.ts
import { renderToString } from 'react-dom/server';

router.get("/", (req, res) => {
  const app = renderToString(<App />);
  return res.send(`<div id="root">${app}</div>`);
});

Then, to make the page interactive on the client-side we run the hydration process with:

// client.tsx
import { hydrateRoot } from 'react-dom/client';

hydrateRoot(document.getElementById('root'), <App />);

So, when I get the page with all my special details from the server, everything nice and pretty, I hydrate it to be able to get interactive events, such as clicking on a button, scrolling, resizing and those actions connect with the page.

Jump to headingWhat is a hydration process 🫗

A hydration process is when we take a dry previously rendered HTML page on the server and hydrate by attaching event handlers for interactivity.

In the React example, this is done by attaching the React application to the root DOM node and hydrating node by node with JavaScript. That also means that these event handlers only get attached when the necessary JS bundle has been loaded and executed, so it can take a while to be fully interactive.

Obviously, we don’t have to do such complex and error-prone task by hand nowadays. Most decent frameworks that are built upon this can handle most cases, and, if not, they can provide documentation for common issues, such as hydration mismatches.


Jump to headingCaching

Caching plays a critical role in Web Rendering, which can have a high or low impact depending on your implementation strategy and needs.

Before heading to the next rendering pattern, I would like you to take a sip of water and go with me to understand first how caching works in the browser! 😉

Jump to headingCache-control

Cache-control is one of the most common HTTP headers used to manage caching in network requests in a very granular and flexible way.

Cache-Control: public, max-age=300

max-age controls how long can it be cached in seconds relative to the time of the request.

public is a directive to tell the browser that the resource can be cached by anyone, CDNs, third-party servers, etc.

max-age example as 5 min battery

As if each resource or file labeled with the max-age had a battery life that each incoming request will result in the same “charged” file as long as the battery don’t dry, which in case that happens the next request fetches the full asset instead of using the cache.


Cache-Control: no-store

no-store is a directive to tell the browser to never cache the file of that resource, so no battery whatsoever. Useful, for example, when we have a page with sensitive or time-based data.


Cache-Control: no-cache

no-cache is used to dynamically serve a file by revalidating with the server, if new content is available, the browser serve that new file received, otherwise, it returns the existing cached version.

diagram battery representing no-cache revalidation
Cache-Control: private, max-age=60

private tells the browser to only keep a copy of that file/response on the client side and prevent the same response from being cached by external servers.

Jump to headingStale-while-revalidate

SWR or state-while-revalidate is a cache invalidation mechanism for re-generating pages.

stale-while-revalidate is a Cache-Control directive for non-critical resources by keeping a stale response while the server regenerates the page for updates.

Cache-Control: max-age=300, stale-while-revalidate=60

In the above example, we tell the browser that the specific resource/file will stay in cache for 5 minutes, and once the user requests that same URL after this time, the browser will kick the revalidation process for 1 more minute (60 sec) so the server can regenerate a new page for the subsequent requests.

So we’re saying that the file can be stale for 1 minute before being served with fresh content.


Now that you have a good grasp of how to do caching of network requests, let’s proceed to the next pattern.

Jump to headingISR

Remember the first example I showed you about SSG where you could generate N pages during build time? Yeah?

Well, it is not a complete solution, as you might imagined if we’re dealing with outdated content that needs frequent updates and bulk of hundred of pages for your site, SSG itself might have some issues it can’t solve itself.

Enter the room ISR.

ISR or Incremental Site Regeneration is an increment over Static Site Generation (SSG) to solve issues of having frequent updates on static content by implementing different caching strategies.

ISR aims to provide a balance between static and fresh content by updating or rendering new pages even after a deploy has been done. If a user requests a new page that wasn’t previously rendered, the server can generate it at the time.

diagram showing the request-response cycle of ISR
  1. The user makes a request
    • If is under the revalidation time, then the server responds with same cached file
  2. The user makes a second request, past the revalidation time.
    • The browser tells the server to re-generate that file
    • Revalidate the cache
  3. The user makes a 3rd request
    • The browser receive the new fresh content from the server and serve that ✨

The revalidation process can either be triggered time-based (using max-age) as we saw previously in caching or on-demand by a function triggered on backend.

One page is generated at a time. The fallback of ISR can be a loading page or 404 page for when we’re waiting the generation process or it can be the previously generated page in case regeneration fails.

This can be very suitable when you have a large-scale project with hundreds of pages, say we want to update a link or a typo in 1 of 100 published blog posts, we don’t need to pre-render all of them, but only the ones that the user can access more quickly.

This can optimize time and cloud computing costs by only pre-rendering a subset of pages.

Jump to headingIsland Architecture

Island architecture or component island is a concept of having small focused islands of interactivity delivered on top of static pages to reduce the amount of JS code you send to the client while still being interactive.

The island Architecture was popularized by the Framework Astro, which they call Astro islands.

An island is essentially any interactive component on the page. It is a progressive enhancement over static content.

Islands run in isolation and can share state with others. For instance, if you had a newsletter component and a subscribers count component as islands, they could operate independently while still communicating with each other as needed.

This idea results in great performance improvements because it eliminates the need to wait for the entire application to be hydrated before it becomes usable. Instead, only the specific interactive components, or islands, are loaded, which reduces the amount of JavaScript sent to the client.

This not only speeds up the initial load time but also enhances the overall user experience by making the interactive parts of the application responsive more quickly.

You can read more about the Islands Architecture in this post.

Jump to headingStreaming SSR

One issue with SSR these days is it doesn’t let components “wait for data.” With the current setup, when you’re ready to render to HTML, you need to have all the data prepped for your components on the server. So, you end up gathering all the data on the server before you can even start sending any HTML to the client. It’s pretty inefficient.

Streaming Server-side rendering is a rendering pattern unlocked by renderToPipeableStream to stream certain parts of your UI directly from the server to the client as the page is being loaded.

If you know Node ReadableStreams, this should look familiar to you.

renderToPipeableStream makes it possible for the app to start loading and processing this information as it’s still receiving the chunks of data from the App component!

As the name suggests, streaming implies chunks of HTML are streamed from the node server to the client as they are generated. As the client starts receiving “bytes” of HTML in the client soon after rendering starts on the server, the TTFB is reduced and relatively constant.

It is also more consistent irrespective of the page size. Since the client can start parsing HTML as soon as it receives it, the FP and FCP are also lower.

import { renderToPipeableStream } from 'react-dom/server';

renderToPipeableStream(<App />)

The client receiving this stream can subsequently call ReactDOM.hydrate() to hydrate the page and make it interactive.

For instance, in Next.js, you can create Suspense Boundaries to wrap components that will progressively sent from the server to the client.

This allow us to handle complex components that don’t need to be rendered immediately with high priority components. Individual components can also show custom loading states as the page is streamed and constructed by the browser.

export default function NewsFeed() {
  <>
    <Suspense fallback={<Spinner />}>
      <OlympicResults />
    </Suspense>
    <Suspense fallback={<Spinner />}>
      <Weather />
    </Suspense>
  </>
}

With this pattern you can get the benefits of:

  • Fast TTFB (Time to First Byte): The browser streams the HTML page shell without blocking the server-side data fetch.
  • Progressive hydration: As server-side data fetches are resolved, the data is streamed within the HTML response. The React runtime progressively hydrates the state of each component, all without extra client round trips or blocking on rendering the full component tree.

Frameworks that implement this pattern are: Next.js and Remix

Jump to headingServer Components

So, a couple years ago, the React Team introduced the concept of React Server Components (or RSCs for short).

React Server Components are components that run exclusively on the server. They allow direct access to external resources, such as SQL databases, the filesystem, and third-party APIs, and use expensive dependencies without needing to send more JavaScript to the client.

It can be as simple as performing data-fetching or a query inside an asynchronous component:

async function BlogPost() {
  const post = await db.posts.get(slug) // some ORM

  return (
    <article>
      <h2>{post.title}</h2>
      <p>...</p>
      <p>...</p>
    </article>
  )
}

This removes the complexity involved in the constant back-and-forth communication between the client and server, making the interaction more streamlined and efficient.

However, it does bring some intrinsic breaking changes you should be aware of… In fact, Server Components themselves have interesting peculiarities that I can’t cover entirely in this post, so I’ll try to focus on one of the pain points.

Jump to headingClient vs Server components

When writing a server component you’re limited to running things on the server, and no having access to the browser APIs or React client features such as: reactive state, context API or built-in hooks.

Besides, you also need to think about distingush about server and “regular” components or Client components when writing code that contains RSC.

Client components are the denoted by the 'use client' declaration at the top of the file.

'use client'
function PostReactions() {
  const [reaction, setReaction] = useState(null)

  const handleClick = (reaction) => setReaction(reaction)

  return (
    <div>
      <button type="button" onClick={() => handleClick('👍')}>👍</button>
      <button type="button" onClick={() => handleClick('👎')}>👎</button>
    </div>
  )
}

RSCs can interop with client components by, for example, having one parent component be a server component and a child being a client component with the ‘use client’. But surprisingly enough, this is not the case in the opposite direction, client components need to run on the client as well, so having server code inside them wouldn’t work out.

That means you need to stop and think a little bit how would organize your application to attend both usages.

server and client components in the tree

Jump to headingPartial Prerendering

In the first sections of this post, I talked about having Static and Dynamic content, but what if we had both?

Partial Prerendering (PPR) is fairly new rendering pattern introduced in Next 14 to combine both static and dynamic rendering on the same page.

Let’s say you have an e-commerce website, you can have a product catalog as a static, and a shopping cart as a dynamic part of your UI, so you get the best of both worlds.

Partial Prerendering is powered by Suspense boundaries in React. When the initial HTML is loaded as static content the user will see a Suspense fallback element for the dynamic UI elements while they’re rendering.

export const experimental_ppr = true;

export default function CommerceShell() {
  <main>
    <header>
      <Suspense fallback={<CartSkeleton />}>
        <ShoppingCart />
      </Suspense>
    </header>
    <ProductsCatalog />
  </main>
}

Yep, it works similarly to streaming SSR where we provide a fallback and let the dynamic content be streamed to the page as it’s ready, but also prerender all static content and access quickly on the first load.

It is quite an experimental feature, and only available in Next.js at the time of this writing.

Jump to headingWrapping up

Phew… that was quite a lot, wasn’t it?

The moral of the story is that each pattern can solve a particular use case. Each one has its pros and cons, being some for well-suited for dynamic content or for static component, or maybe both.

Moreover, the landscape of web rendering is constantly evolving. New patterns and techniques are being developed to address emerging challenges and opportunities.

Thanks for reading!