SSR vs SSG in Next.js – a practical overview for CTOs and devs

First, we got JavaScript. Then, we got Single Page Applications. Then, we got… a headache. That’s because our pretty and highly interactive web apps became slow and went through a number of usability issues, affecting both search robots and human beings. Developers frantically searched for solutions. Are Server-Side Rendering and Static Site Generation the answer? To find out, let’s take a look at Next.js, one of the most popular SSR/SSG frameworks out there.

There are so many fancy buzzwords in the world of software development that if I really wanted to, I could fill an entire article with nothing but a long list of pretty terms found at the crossroad between software technology and business.

But SSR and SSG really do stand out. They are of genuine and practical importance to everyone involved in ‌making software – from developers to designers, UX specialists, testers, web analysts, SEO experts, copywriters, and more.

The benefits of using SSR and SSG skilfully make a big difference in all of these areas. If you don’t know that yet, I’m sure you will see that by the time I wrap things up.

Today, you’re going to receive practical insights into SSR and SSG. I’m also going to introduce a framework that makes it really easy to use them – Next.js.

What this article is:

  • an overview of key SSR and SSG concepts and benefits of both approaches,
  • an introduction to Next.js and its benefits for business,
  • a practical and easy-to-understand explanation of how to implement SSR and SSG using Next.js,
  • a business case for and against using SSR and SSG with Next.js in specific scenarios (pros, cons, and alternatives).

What this article is not:

  • an in-depth technical analysis of Next.js-based SSR and SSG. 

While I’m going to get into some code, I’m keeping things approachable for a wider audience. I really do want to show how SSR and SSG relate to the needs of businesses in modern-day software development.

A quick look at history – why do we need SSR and SSG?

To understand the need for SSR and SSG more clearly, let’s take a big jump into the past. It’s a rainy and cold afternoon on December 4th, 1995…

Early days of JavaScript

… Brendan Eich is just completing his work on the very first version of JavaScript. It is being shipped by Netscape as part of its Netscape’s Navigator browser.

JavaScript quickly gained popularity as a scripting language on the client-side. In the old days, it was mostly used for form validation and basic page interactivity on a typical HTML page. The following years saw all kinds of innovations.

First, the Node.js environment provided a way to use JavaScript on the server as a full-fledged backend language. Then, modern web frameworks and libraries such as React or Vue.js highly extended JavaScript’s capabilities in terms of support for user interaction. As a result, developers could create a web page or app that felt much like native desktop and mobile applications. They worked really fast and required little to no reloading in the browser. Users loved it. Developers loved it and they called it…

… Single Page Applications

The native-like performance made SPAs popular in modern web development of that time, but not everyone shared the enthusiasm. SPAs certainly did not earn the love of search engines.

The HTML content rendered on the client-side was only available to them after it was executed in the browser. As a result, automatic crawlers dispatched by search engines missed most of it. That made SPAs very difficult to index properly. In turn, websites missed out on a large share of their organic traffic – a big problem in terms of search engine optimization. Just when SEO was really getting big in the first decade of the 21st century…

Brendan Eich was one of the key people in the early development of JavaScript

SSR vs SSG – the theory

SSR and SSG are the two techniques that evolved in the software development community to solve this very problem. What are they?

What is Server-Side Rendering?

With SSR, you can render the JavaScript code on the server and send indexable HTML to the user. The HTML is thus generated during runtime so that it can reach search engines and users at the same time.

Before Next.js showed up, such a process required a lot of tweaking and came with issues related to server load, on-demand content, caching, or even the application architecture itself. Don’t get me wrong – it was certainly doable, but it would have been nice if one could dedicate all that time to developing business logic instead…

What is Static Site Generation?

The major difference between SSR and SSG is that in the latter’s case, rather than during runtime, your HTML is generated during build time. Such websites are extremely fast since the HTML content is served even before you make a request. On the other hand, the website needs to be rebuilt and reloaded entirely every time a change is made. Consequently, SSG-based websites are far less interactive and native-like than those that rely on SSR. They are largely static sites with little to no dynamic content.

The basic idea behind SSR and SSG

SSR and SSG – main benefits

Both SSR and SSG share some major benefits for business.

  • SEO

Easily indexable pages translate into measurable benefits – more organic traffic. For web apps that consider SEO strategically important for marketing and sales, it can further translate into improved profits and overall bottom line. Reports by SEO agencies confirm that – this e-commerce store improved its sales by 67% due to an increase in organic traffic.

  • Performance

UX research by Google shows that the bounce rate increases by as much as 32% if the page load goes from 1 to 3 seconds. SSR and SSG both improve the loading time when compared to rendering on the client-side. That’s because SSR collects all the ‌data without waiting for the browser to make such a request. SSG goes even further, collecting all the data as it builds the application.

  • Usability

By doing more of the heavy lifting on the server rather than in the browser, you relieve client browsers and devices. As a result, SSR and SSG make it easier for users with older devices and slower internet connections to view a web app. This may be especially important for mobile conversions – as many as 73% of mobile users struggled with slow-to-load websites.

Think With Google is an excellent source of SEO and UX insights – after all, the Google search engine is what SEO-conscious developers and marketers try to please most

By now, I’m sure you can see the benefits of SSR and SSG. Let’s find out how you can realize them using Next.js.

Server-side rendering with Next.js

SSR pages are generated upon each request. The logic behind SSR is executed only on the server-side. It never runs in the browser.

How does Next.js-based SSR work?

If you export a function called getServerSideProps from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps.

The getServerSideProps method is called in two scenarios:

  • when a user requests the page directly,
  • or when a user requests the page in a client-side transition by using next/link or next/router.

The getServerSideProps function always returns an object with one of the following properties:

  • props – contains all the data required to render a page.
  • notFound – allows the page to return a 404 status and a 404 Page.
  • redirect – allows redirecting the user to internal and external resources.

Interestingly, Next.js claims that the entire code used in getServerSideProps should be eliminated from the client bundle so that you can create server-related and client-related code. It’s not always true. One wrong import or dependency and the bundling will not work as expected. The more straightforward the structure of your application is, the easier it is to achieve proper behavior.

An implementation example:

import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';

type Product = {
 id: string;
 title: string;
}

type SearchResultsPageProps = {
  products: Array<Product>;
}
 
const SearchResultsPage = ({ products }: SearchResultsPageProps) => (
    <div>
        {products.map((product) => (<p key={product.id}>{product.title}</p>))}
    </div>
)
 
export async function getServerSideProps({ res, query }: GetServerSidePropsContext): Promise<GetServerSidePropsResult<SearchResultsPageProps>> { 
  const category = query?.category;
  
  if (!category) {
    return {
        redirect: {
            destination: '/'
        }
    }
  }
  
  const data = await fetch(`http://external-api.com/products?category=${category}`);
  const products = await data.json();
  
  if (products.length === 0) {
    return {
      notFound: true
    }
  }
 
  return {
    props: {
      products,
    }
  }
}
 
export default ProductsPage;

As you can see, it’s quite self-explanatory:

  • you can handle a query parameter such as a category
  • if the category parameter does not exist, you can return a redirect object. Next.js will redirect the user to a given destination
  • if the parameter exists, you can proceed and make a call to an external API
  • if the data from the external API is empty, you can return a 404 page
  • if you have any item, you can properly render your page with a list of products

When to use SSR?

SSR is recommended for apps in which you have to pre-render frequently updated data from external sources. This technique is especially recommended when the data cannot be statically generated before a user request takes place, and at the same time needs to be available to search engines.

Example? A search results page or an e-commerce website that includes user-generated content.

Pros of SSR:

  • the page always contains up-to-date content,
  • if an error is thrown inside getServerSideProps, Next.js will show an error page automatically
  • you have access to cookies, request headers, and URL query parameters
  • you can implement logic related to the 404 page, and redirects based on user requests and data

Cons of SSR:

  • the page is noticeably slower than a statically generated one because some logic needs to be executed on every request (e.g. API call).
  • a server-side rendered page can be cached on CDNs only by setting the cache-control header (it requires additional configuration).
  • the Time to First Byte (TTFB – the time it takes a server to deliver the first byte of information to a page) metric will be higher than on a statically generated page

Onto SSG!

Static site generation with Next.js

SSG-based pages are generated at a build time. You can reuse (and reload) the whole page on each request.

How does Next.js-based SSG work?

Next.js pre-renders pages using static generation, which among other things means that it doesn’t fetch any data by default. If you need to generate a page that includes such data, you have two scenarios:

  • if the page content depends on external data, use getStaticProps method only,
  • if page paths depend on external data – use the getStaticPaths method in addition to getStaticProps.

The getStaticProps method always runs on the server (as opposed to the client-side). It collects page data for a given path. The method is called in one of three cases:

  • during the next build,
  • in the background when you use revalidate,
  • on-demand in the background when using unstable_revalidate.

I’m going to go over the last two cases in the Incremental Static Regeneration section.

The getStaticProps method always returns an object with one of the following properties:

  • props – it contains all the data required to render a page,
  • notFound – allows the page to return a 404 status and a 404 Page,
  • redirect – allows redirecting the user to internal and external resources,
  • revalidate – the time (in seconds) it takes for a page regeneration to occur.

What is really important, the getStaticProps method does not have access to incoming requests. If you need access to the request, consider using ‌some middleware besides getStaticProps or consider SSR instead. In development mode, getStaticProps is called on every request for a better developer experience.

The getStaticPaths method will only run during a production build. It will not be called during runtime. The method must be used together with getStaticProps. It cannot be used with getServerSideProps.

The getStaticPaths method always returns an object with any of the following properties:

  • paths – determines which paths should be pre-rendered at build time,
  • fallback – a boolean flag that determines how the app should behave in case the user wants to visit a page that wasn’t listed in the paths array.

Next.js has provided On-demand Static Regeneration in version 12.1. It means that you can manually purge the static cache for a ‌page if its content has been updated in an external source (e.g. a CMS or database). The function gives you the ability to always deliver up-to-date data with no delays and still keep static generation for a given page.

An implementation example:

import { GetStaticPathsResult, GetStaticPropsResult, GetStaticPropsContext } from 'next';

type Post = {
 id: string;
 title: string;
}

type PostPageProps = {
  post: Post;
}
 
const PostPage = ({ post }: PostPageProps) => {
  return (
    <p>{post.title}</p>
  )
}
 
export async function getStaticPaths(): Promise<GetStaticPathsResult> {
  const data = await fetch('http://external-api.com/posts');
  const posts = await data.json();
  
  return {
    paths: posts.map(post => ({
      params: { id: post.id }
    })),
    fallback: false,
  }
}
 
export async function getStaticProps({ params }: GetStaticPropsContext<{ id?: string }>): Promise<GetStaticPropsResult<PostPageProps>> {
  const data = await fetch(`http://external-api.com/posts/${params.id}`);
  const post = await data.json();
 
  return {
    props: {
      post,
    },
  }
}
 
export default PostPage;

The example above implements the second of the two previously mentioned scenarios – when paths depend on external data:

  • you implement the getStaticPaths method to get a list of blog posts based on a response from an external API,
  • the getStaticPaths method returns an array of paths, which will be pre-rendered at build time,
  • fallback: false returned by getStaticPaths means that application will present a 404 page for any page not listed in paths,
  • with the getStaticProps method, you fetch the data for a single post and return the data as page props to a page component.

When to use SSG?

SSG is recommended for use on any page where you have to pre-render data. It can be generated before a user request takes place. It means that your data is available at build time, or in other words – on every page where you want to present static content or provide excellent SEO capabilities. Examples of such pages include blogs or marketing sites that contain data from a headless CMS the content of which is not updated very often.

Pros of SSG:

  • you can boost performance using CDN caching without a lot of extra configuration,
  • your static page is always online even if your backend or data source goes down,
  • your page is much faster than a server-side rendered one because the entire logic was executed at build time
  • your backend serves only static files, which contributes to decreasing the server load,
  • you can run your statically generated page in the preview mode; the page is then rendered at request time.

Cons of SSG:

  • due to the lack of access to incoming requests, you cannot read request headers, cookies, or URL query parameters,
  • your content cannot be changed between site deployments (without ISR).

As you can see, both SSR and SSG have their pros and cons. That’s why you should also consider alternative solutions.

SSR and SSG alternatives

With all that said and done, one could think that SSR and SSG exhaust all the possibilities of moving your workload to the server. But developers really can’t help themselves but innovate all the time. Enter Incremental Static Regeneration!

Incremental Static Regeneration – what is it?

Incremental Static Regeneration is one of the most powerful features in Next.js. It enables you to use static generation in a way that doesn’t force you to rebuild your whole page on ‌every reload.

Incremental Static Regeneration is an extension of static generation. It offers an additional property returned by getStaticProps. This property is called revalidate and is counted in seconds. The revalidate property tells you how often Next.js should regenerate static HTML for a given page. The regeneration is triggered by a user request rather than every “X” seconds. That property should be configured based on the nature of data you want to show on a given page like blog posts, products, user-related data, marketing content.

ISR improves the scalability of web applications. You can statically generate hundreds of the most popular or latest posts at build time and enable ISR for the rest of the articles. Once a user makes a request to a page not listed in getStaticPaths, Next.js will server-render the page for that user and statically generate the page in the background. The next user will receive statically generated content. It will keep the build time short, retaining all the benefits of SSG for every blog post. Imagine how long the build can take if you want to pre-render 10,000+ posts at build time!

An implementation example:

import { GetStaticPathsResult, GetStaticPropsResult, GetStaticPropsContext } from 'next';

type Post = {
 id: string;
 title: string;
}

type PostPageProps = {
  post: Post;
}
 
const PostPage = ({ post }: PostPageProps) => {
  return (
    <p>{post.title}</p>
  )
}
 
export async function getStaticPaths(): Promise<GetStaticPathsResult> {
  const data = await fetch('http://external-api.com/posts');
  const posts = await data.json();
  
  return {
    paths: posts.map(post => ({
      params: { id: post.id }
    })),
    fallback: 'blocking'
  }
}
 
export async function getStaticProps({ params }: GetStaticPropsContext<{ id?: string }>): Promise<GetStaticPropsResult<PostPageProps>> {
  const data = await fetch(`http://external-api.com/posts/${params.id}`));
  const post = await data.json();
 
  return {
    props: {
      post,
    },
    revalidate: 10, // In seconds
  }
}
 
export default PostPage;

How does it work?

You fetch a list of posts to pre-render in getStaticPaths the same way you do for SSG. The difference is that the fallback value is set to blocking. It means that when the user visits a page that is not statically generated yet, Next.js will return content after server-rendering is done for that particular page. SSG will be done in the background for the users that follow.

In getStaticProps, there is a new property tasked with returning an object called revalidate. It is counted in seconds so Next.js will pre-render the page when a request comes, but at most once per 10 seconds. When a page is visited just once per day, revalidation triggers once per day.

When can you consider ISR?

  • When you want to refresh the content without having to rebuild the entire site.
  • When you have hundreds of pages that you have to pre-render but you do not want to spend hours on building an app.
  • When you want to statically generate pages that will be created soon – for example by content editors. When they create a new blog post, you do not want to have to rebuild your app to generate a newly created page.

CSR or Client-Side Rendering

Client-side rendering based on client-side data fetching is another alternative. Unlike in the case of working with server-side rendering API, data fetching can be done on a page or component level. 

Client-side data fetching can affect the performance of your application and the loading speed of your pages negatively. That’s because data fetching is done when the component or page is mounted and the data is not cached.

When can you consider CSR?

  • when your acquisition strategy doesn’t prioritize SEO,
  • when you don’t need to pre-render your data,
  • when the content of your pages needs frequent updating,
  • when the page content is related to logged user data.

When you do decide to use SSR or SSG, should you always implement it using Next.js?

Why Next.js for SSR and SSG?

Next.js is a powerful solution that offers a lot of different approaches to rendering your pages. Since it covers so many techniques, you can easily adjust it to your requirements. You can use different rendering strategies or even combine them together.

How to figure out which approach is the best? Here are a couple of helpful questions to ask yourself:

  • Do you need both excellent SEO and performance? SSG is the way to go.
  • Do you need SEO and the ability to frequently update your data? Use SSR.
  • Do you require frequently updated data but SEO and performance are not a priority? CSR is a sensible choice.
  • Do you have thousands of pages that require frequent updating of content? ISR is worth considering.

There are multiple combinations of the scenarios above. Finding the best balance requires a thorough analysis of your project.

Another reason for choosing Next.js is straightforward, and yet very important – Next.js is extremely popular. It has over 83,000+ stars on Github (more than any other alternatives) and a big community. Furthermore, the Vercel company works in the background, ensuring continuous development and maintenance of the Next.js framework. Combined, you have a stable, vibrant environment that is here to stay.

Of course, as with every framework, Next.js has its disadvantages. For example, it’s not that easy to share static content between multiple Next.js instances in the production environment using Docker. Still, the flexibility of Next.js makes it relatively easy to find solutions in the event of changing requirements in the project. Most likely you won’t need to migrate to another solution or framework to get things done.

Spread the love