DEV Community

Cover image for Rendering strategies: CSR, SSR, SSG, ISR
Souvik Jana
Souvik Jana

Posted on

Rendering strategies: CSR, SSR, SSG, ISR

We have been using frontend libraries/frameworks that help to build single-page applications out of the box where the application is rendered on the client side and we all are pretty familiar with that approach.

There're a few more ways to render applications which we would be covering in this article along with a brief discussion of client-side rendering(CSR), just to see how it's different from pre-rendering.

Client-Side Rendering(CSR):

In this strategy, the server sends a barebones HTML with just an empty <div> tag, and then data fetching, generating the page with the data, routing, etc. are handled by JavaScript(JS) on the client side(browser).

Pros:

  • There’s no round trip to the server to fetch more pages once the initial load is completed, so the user experience becomes really good, almost like a native app experience.

Cons:

  • Not Search Engine Optimization(SEO) friendly as the page is client-side rendered and web crawlers understand server-side rendered pages.

  • Initial page load takes quite some time because most of the web page lifecycle-related tasks are handled by JS which leads to bad First Contentful Paint(FCP), and Time To Interactive(TTI) for the page. Depending on the complexity and size of the app, this increases more. This implies that the user will see almost a blank screen during the time between First Paint(FP) and FCP.

Server-Side Rendering(SSR):

Unlike CSR, in this strategy, the page would be generated on the server side with all data. So, the JS required to render the page won’t be shipped to the client and additional network requests to fetch content to render the page can also be avoided in this way.

In the case of server-rendered pages, the server sends the rendered non-interactive HTML to the client, and then the client downloads the JS bundle to hydrate or make the page interactive by adding event listeners. This process is known as hydration. Read more about hydration and its implementation here.

Steps to generate pages using SSR

Pros:

  • Lesser JS for the client to get parsed leads to better FCP, and TTI and provides an additional budget for client-side JS.

  • SEO enabled as the page is now server-side rendered and crawlers can use the same to index it.

Cons:

  • Slow Time To First Byte(TTFB) since we need to generate the page on the server, users will see a blank page for that time 😟.

  • Full page reload is required for some interactions.

Implementation:

React frameworks like NextJS, Remix, Gatsby, etc. make it easier to implement SSR. We'll use NextJS to show the implementation since it supports all the rendering strategies that we'll be discussing here.

NextJS provides some functions out of the box to implement pre-rendering and getServerSideProps() is one of them to implement server-rendered pages.

import Link from "next/link";

const Home = ({ posts }) => {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
      }}
    >
      {posts.map((post) => (
        <Link href={`/${post.id}`} key={post.id}>
          <Post {...post} />
        </Link>
      ))}
    </div>
  );
};

export async function getServerSideProps() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();

  return { props: { posts } };
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

getServerSideProps would be executed at run time to fetch some data from external APIs or filesystem and pass that data to the component(in the above example to Home). Finally, the page would be rendered on the server and sent back to the client.

To know which strategy is being used to generate different pages in a nextJS app, we can build the app locally and that would give the following result:

Build log of SSR implementation with NextJS

The build result shows the rendering strategy in use, generated pages during the build with the total time the pages took to be built, and other details for each page. It depicts the rendering strategy using some symbols:

ƛ(lambda for server-side rendered pages),

○(circle for static pages),

●(filled circle for static site generated pages).

In our implementation, we've used SSR to generate the home page and the build log also tells the same using ƛ beside the home(/) route.

Static Site Generation(SSG):

SSG solves all the problems introduced by CSR and SSR by generating pages of an app at build time with all the data. So, in this way, the server won’t have to render the page, and the client would also require minimal JS to make the pages interactive which leads to faster TTFB, FCP, and TTI.

Steps to generate pages using SSG

Pros:

  • Great performance as the page would be built during build time and ready to be served immediately.

  • SEO friendly as the pages are pre-rendered at build time and available on a server.

Cons:

  • Generating a large number of HTML files during build time can be challenging and time-consuming.

  • Any update in data would require the app to go through the build process to generate the pages again with the latest data - which is not that great!

  • Not so good for highly dynamic content.

Implementation:

There're 2 methods - getStaticProps() and getStaticPaths() that can be used to render static pages. So, if we have a page, for example, to show all posts, we can use getStaticProps() only to fetch data and generate the page whereas getStaticPaths() could be used along with getStaticProps to generate a page for an individual post.

import Link from "next/link";

const Home = ({ posts }) => {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
      }}
    >
      {posts.map((post) => (
        <Link href={`/${post.id}`} key={post.id}>
          <Post {...post} />
        </Link>
      ))}
    </div>
  );
};

export async function getStaticProps() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();

  return { props: { posts } };
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

The above implementation would show all the posts on the home page. The implementation is almost similar to SSR, only the method name is different and page generation would happen during build time.

Now, to generate pages for individual posts, we can use getStaticPaths to fetch all posts and take unique IDs, so that a unique route can be generated for each post with the help of dynamic routes features provided by NextJS.

// generating list of paths/page urls of posts using unique IDs of posts
export async function getStaticPaths() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();

  const paths = posts.map((post) => ({ params: { id: `${post.id}` } }));

  return { paths, fallback: false };
}

// fetching the post details for the given id(ids would come from the result of `getStaticPaths()`) and passing it to the component
export async function getStaticProps({ params }) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`
  );
  const post = await response.json();

  return { props: { post } };
}

// rendering the given post details
const PostDetails = ({ post }) => {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh",
      }}
    >
      <Post {...post} />
    </div>
  );
};

export default PostDetails;
Enter fullscreen mode Exit fullscreen mode

If you see the getStaticPaths() method in the above snippet, we have used fallback: false that means if a user tries to access a page that is not available then the 404 page would be shown.

This is what the build log of SSG implementation looks like:

Build log of SSG implementation with NextJS

As per our implementation and the build result, we can see that the home page and 100 post details pages are using SSG with build time for each.

Incremental Static Regeneration(ISR):

ISR solves the problems with SSG by generating a subset of pages at build time and the rest of the pages on demand. Also, it provides a mechanism to regenerate the pages again if there’s any update in data.

ISR is only supported by NextJS as of today which provides a property called revalidate (which takes an interval in seconds) to validate the data and trigger a page regeneration.

Does that mean that the pages are regenerated continuously at the given interval?

Nope, it regenerates a page once there's any update in data and the given interval is over after that.

Even after the given interval, the first visitor of the page will still see the page with stale data and the page would be regenerated with the latest data in the background. Once the page is generated successfully, it'll replace the old page. If the page regeneration fails for some reason, the old page remains unaltered.

Steps to generate pages using ISR

Pros:

  • This way of rendering pages comes with almost all the benefits of SSG and it reduces the build time of an application as it avoids pre-rendering all the pages during build time.

  • Regenerates pages if there’s any update in data without rebuilding the whole application.

Cons:

  • When users visit a page, we want them to see the most up-to-date version immediately. With ISR the first visitor to a page will not see that. They will always see a fallback first or a page with old data. And then later, if the data gets stale, the first visitor to see that cached page will see the out-of-date data first before it revalidates.

Implementation:

We use the getStaticProps method only to implement ISR but use an extra property revalidate while passing the data to the component.

So, the implementation with ISR for the home page would change as follows:

import Link from "next/link";

const Home = ({ posts }) => {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
      }}
    >
      {posts.map((post) => (
        <Link href={`/${post.id}`} key={post.id}>
          <Post {...post} />
        </Link>
      ))}
    </div>
  );
};

export async function getStaticProps() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();

  return {
    props: {
      posts,
    },
    revalidate: 10,
  };
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

And the implementation with ISR for the individual posts page would change as follows:

// ISR - fallback: 'blocking'. Restrict in `getStaticPaths()` to generate only limited number of pages and generate rest of the pages on demand
export async function getStaticPaths() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();

  const paths = posts
    .slice(0, 15)
    .map((post) => ({ params: { id: `${post.id}` } }));

  return { paths, fallback: "blocking" };
}

export async function getStaticProps({ params }) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`
  );
  const post = await response.json();

  return { props: { post }, revalidate: 10 };
}

// ISR - fallback: 'blocking'
const PostDetails = ({ post }) => {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh",
      }}
    >
      <Post {...post} />
    </div>
  );
};

export default PostDetails;
Enter fullscreen mode Exit fullscreen mode

If you've noticed the code for the post details page, we've used blocking as the value of fallback , which means the rest of the pages would be generated server-side on demand. The other possible value of fallback is true and in that case, we can show the fallback page while the page is getting generated.

import { useRouter } from "next/router";

// ISR - fallback: true.
export async function getStaticPaths() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();

  const paths = posts
    .slice(0, 15)
    .map((post) => ({ params: { id: `${post.id}` } }));

  return { paths, fallback: true };
}

export async function getStaticProps({ params }) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`
  );
  const post = await response.json();

  return { props: { post }, revalidate: 10 };
}

// ISR - fallback: true, show fallback page if value of fallback is true
const PostDetails = ({ post }) => {
  const router = useRouter();

  if (router.isFallback) {
    return <Loader />;
  }

  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh",
      }}
    >
      <Post {...post} />
    </div>
  );
};

export default PostDetails;
Enter fullscreen mode Exit fullscreen mode

Now, let's see how the build process of ISR is different from the build of an application using SSG.

Build log of ISR implementation with NextJS

As per the logs above, we can see that we're generating 15 pages for individual posts instead of all individual posts during build time which reduces the build time significantly and we're revalidating any updates in data at an interval of 10 seconds.

That's all for now 😃. Thanks for reading till now 🙏.

If you want to read more about these strategies, refer to Rendering Introduction.

Share this blog with your network if you found it useful and feel free to comment if you've any doubts about the topic.

You can connect 👋 with me on GitHub, Twitter, and LinkedIn.

Top comments (15)

Collapse
 
pierrewahlberg profile image
Pierre Vahlberg

I would like to point out two things in this overall very well written and covering article (because it is good!).

First off, it sounds like you are referring to Single Page Applications when you write about CSR. It might be me who's old but client side rendering can happen even when SSR is used, and it is definitely used outside of SPAs, for example on light html sites with low interactivity or low dynamic content.

Also, the old myth that crawlers (which every one says but we all mean google) can not parse CSR sites or SPAs has been debunked. They do in fact use strategies to look at page contents even if you use SPA, dynamic content or just lousy lazy loading so its not a total no-go for SEO. But, there should be a fair warning that if you are building a SEO reliant site and you are going for a SPA you should ask yourself why you ended up in that decision 😁👌

Cheers mate and thanks for a good read!

Collapse
 
sureisfun profile image
Barry Melton

In practice, search engines can parse CSR pages, but unless you have a very popular site, they probably won't.

Something I should probably do a post on is the concept of a 'crawl budget' -- if you have a new site, and submit it to Google Search Console, it is extremely unlikely that Google will crawl your pages even if they are perfect. This is because they a) are trying to be good citizens and not hugging a new site to death, and b) because they think it's unlikely that your site has value. They may crawl a page or five and may or may not index those pages, but until you show that you have value, the amount of their crawl budget that they're going to spend on your low-authority, brand-new site is very, very small.

CSR sites have to spin up an interpreter, and are parsed more slowly, which means they consume more of your crawl budget, which means it's less likely that all of your pages will be crawled. Google also does a full page refresh on every page scan, which means unlike your users, they have to download the entire React (or whatever) framework with every page they crawl. This further eats into the budget.

You can overcome this by showing Google that you have value. Traffic hitting your site shows value, as do links, and all the other common off-site SEO methods, and you can eventually get popular enough that they'll upgrade your crawl budget a bit at a time. If you become a top x destination site like Facebook or Twitter, then it won't matter at all that your site is CSR and you'll have won, but that's obviously hard, and up until you're the world's most popular site, you can get a lot more crawling done by implementing basically any kind of SSR in the early days to maximize your crawl budget, which usually equates to more pages crawled more quickly.

Collapse
 
sjsouvik profile image
Souvik Jana

Hey, thanks for adding these points and I totally agree with your points 😃. It's just that covering every possible combination of these strategies and everything about these strategies in a single article is difficult 🥲.

And just to cover the basics of these along with implementation, it became the longest article that I wrote so far. Thanks for reading it and sharing your feedback 😃

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
sjsouvik profile image
Souvik Jana

Thanks, glad that you found it useful! :)

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍

Collapse
 
brense profile image
Rense Bakker

I want to switch to nextjs so bad... If only they added support for vite instead of webpack. Great article explaining the different render strategies!

Collapse
 
sjsouvik profile image
Souvik Jana

Thanks 😊. Vite is ❤️

Collapse
 
kumarkalyan profile image
Kumar Kalyan

Great Article @sjsouvik

Collapse
 
sjsouvik profile image
Souvik Jana

Thanks 😊

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

ISR is not a rendering Strategie.

Collapse
 
sureisfun profile image
Barry Melton

I think this is 100% technically correct, but semantically, if you have a slowish release cycle, and want the benefit of fallback data from caching, but don't want to have to generate on each page request, and also don't want to have to delay page updates for a week until the next build cycle -- then ISR is "the answer"

For that reason, I appreciate its inclusion in the article as a strategy.

Collapse
 
sjsouvik profile image
Souvik Jana

Ok, would you like to tell why do you think so?

Collapse
 
ivan_jrmc profile image
Ivan Jeremic • Edited

ISR i a framework feature to update a file on the file system in my opinion.

Thread Thread
 
pierrewahlberg profile image
Pierre Vahlberg

I could agree that it is in some sense a cache busting feature for SSR or or statically generated pages. But the "solution" becomes something you could call ISR