Next.js SSG vs SSR Part 2: Backend Fetching

Next.js SSG vs SSR Part 2: Backend Fetching

Welcome to the second article of the Next.js SSG vs SSR series!

In the previous article we took a brief introduction into what the static site generation (SSG) and server-side rendering (SSR) are, and how these two rendering modes are implemented by the Next.js framework. We've also made a practical experiment to see how SSG and SSR perform for rendering static pages in Next.js.

Today we are going to move one step further and test out the SSG and SSR modes for rendering a page that fetches data from backend server.

With basic undestanding of Node.js and with experience from the first article of the series you will be able to proceed.

Task Overview

Previously we were testing how Next.js would render a static homepage of your business website. That page didn't have any dynamic client behavior nor backend interactions and was supposed to be speed-optimized for SEO purposes.

But let's imagine that you have blog on your business website to enhance customer experience. It means that your website would have a main blog page which displays a list of all articles.

Of course, that article list is not a static content of the blog page. In our example, the page will get that content by making a request to a blog backend server before displaying the article list to a user.

That is basically what we are going to do in this article. We will create a blog page that fetches article list from backend and test out how each of the two rendering modes (SSG and SSR) will render it.

We will figure out the differences of each mode's work and make conclusions how to render a backend fetching page in the most effective way.

The Target Code

For experimenting purposes of today's task we'll keep using the demo application from the previous article, more precisely its extended version, which contains the code we will need for current experiments.

To get started, you should clone the application from its repository:

git clone https://github.com/toxa16/nextjs-ssg-vs-ssr-demo.git

and checkout to the target version:

git checkout a3e47bb9aa5c5b5dfbf715e3f5cfe65e924be22f

And, of course, install dependencies:

npm install

Now, when you're all set, let's walk through the key parts of the demo app, which are significant for today's experiments.

I'll say in advance that the backend-fetching blog page functionality will differs depending on a rendering mode (whether SSG or SSR). For this reason, I've created for you two different blog pages for each of rendering modes.

Both pages use the same presentational Blog component to display the fetched article list. Its code is located in a file components/blog/index.js. It is worth mentioning that the Blog component is a part which provides the loading state rendering to gracefully handle asynchronous backend requests.

Let's now take a closer look at each of the blog pages.

The useEffect Page

The SSG blog page comes first. To see its code, let's open a file pages/ssg/blog.js:

// ...
export default function BlogSSG() {
  const [articles, setArticles] = useState();

  useEffect(() => {
    // defining and calling async function inside useEffect
    (async function() {
      const _articles = await fetchArticles();
      setArticles(_articles);
    })();
  });

  return <Blog articles={articles} />;
}

As we can see, the BlogSSG page component calls a fetchArticles helper function in a useEffect to request data from backend. It then displays the fetched content via the Blog presentational component we've mentioned above.

Thus, after compiling, the SSG blog page will be a statically generated page which calls article fetching inline script after DOM loading is completed. (But don't forget that it is just a hypothesis, which we are up to verify in our experiments).

Due to the page's file location pathname, its route will be /ssg/blog.

The getInitialProps Page

The SSR blog page is a bit different. To see the difference, let's open the page component code file pages/ssr/blog.js:

// ...
function BlogSSR({ articles }) {
  return <Blog articles={articles} />;
}

BlogSSR.getInitialProps = async () => {
  const articles = await fetchArticles();
  return { articles };
}

export default BlogSSR;

As we can see, the BlogSSR page component doesn't have a useEffect statement. Instead, there is a getInitialProps method calling the same helper function fetchArticles.

Via the getInitialProps API, the article list returned from the fetchArticles is passed to the BlogSSR component (notice the articles property) to be displayed by the Blog presentational component.

The SSR blog page route is respectively /ssr/blog.

You have already noticed that both pages reuse the same fetchArticles helper function. That's because for rendering modes it is not so important how the data is fetched, but where it is done.

Simulating Latency

There is one key point left: the blog backend server. It is a tiny Express app with an /aticles GET endpoint, which responds with dummy list of articles stored in the code itself (in /backend/articles.js).

There is one special thing about the server. Let's look at the /articles endpoint in a file /backend/app.js:

// ...
app.get('/articles', (req, res) => {
  // simulating 1 second latency
  setTimeout(() => {
    res.json(articles);
  }, 1000);
});

// ...

See the setTimeout statement? By setting a timeout in the endpoint callback function we simulate a 1 second server latency, so our experiment results are more obvious and transparent.

SSG vs SSR: The Blog Page

Alright, now, after reviewing the most significant parts of our target demo app, we are ready to test on real metal how our blog page(-s) perform under each of the SSG and SSR modes.

Here is how our blog page looks (in its loaded-completed state):

Blog Intro

This is the SSG blog page variant. The SSR one looks identically.

The payload of this page is a list of three dummy article cards, which is supposed to be fetched from backend. Under the hood, this is basically a view of the Blog presentational component, which we mentioned several times in the previous section.

Now, let's start our experiments.

Blog Page SSG

First, let's build and launch our app and its backend:

npm run build
npm start

The npm start command launches in parallel the production Next.js server hosting our app and the blog backend server.

Now we will visit localhost:3000/ssg/blog in a web browser to see the SSG blog page:

Blog SSG Loading

We see it is in a loading state. In a short time, the loading indicator is replaced by article cards: the content has been fetched from the backend.

Blog SSG Loaded

The request timeline of "Network" tab of development tools explains us what has just happened. A static HTML page has been requested from the hosting server, and it didn't take too long for it to get loaded in the browser ("Load" time - 410 ms). Then the inline client scripts make an AJAX request to the backend server's /articles endpoint:

Blog SSG AJAX Timeline

Thus, since there is a 1 second simulated latency on the server side, we have to wait this time watching the loading state of the blog page until the article list appears ("Finish" time - 1,43 s).

To make sure these experiment results are not accedental, I reload the SSG blog page on my local machine 10 times and gather the statistical data of the page loading time:

Blog SSG Chart

Avarages of the 10 measurements (blog page - SSG):

  • DOMContentLoad - 44.3 ms;
  • Load - 404.9 ms;
  • Finish - 1.411 s.

There is one extra moment to pay attention at. It is about the fetchArticles helper function. Let's look at its code under lib/fetch-articles.js (code excerpt):

// ...
async function fetchArticles(baseUrl = process.env.BACKEND_URL) {
  console.log('Fetching articles...');
  // ...
}

export default fetchArticles;

See the console.log statement on the first line of the function body? Now, if you open the "Console" tab of browser development tools, you'll see the following output:

Blog SSG Browser console

It demonstrates that the SSG blog page fetches content from backend via an AJAX call in a browser.

Blog Page SSR

When everything is clear about the SSG blog page, let's try out the server-side rendered one.

Since we haven't made any code changes, there is no need to rebuild our app and/or to restart servers. If you happened to shut the server down, you can simply rebuild and restart everything:

npm run build
npm start

Now, let's load the SSR blog page in our web browser by visiting localhost:3000/ssr/blog:

Blog SSR Loaded

We got the blog page already fed with the article list. No loading state. However, we had to wait about 1.5 seconds before seeing the page ("Load" time - 1.47 s). The request timeline confirms it:

Blog SSR Request Timeline

We waited an entire second before receiving the first byte (TTFB) from our hosting server, and then about half a second more for the page to get loaded in the browser (which is comparable to the SSG blog page "Load" time).

On the other hand, we have received a full, content-ready page, this can be proved out by viewing the page source or the HTTP response body. No AJAX calls to the backend server.

Remember we have stated that the SSR blog page uses the Blog presentational component to display content? Also, remember that the Blog component implements loading state?

Nevertheless, we haven't seen a loading state on the SSR blog page. It is not absent or disabled for the SSR page. It is just never triggered, because the SSR page is eventually generated with the content in place.

But we are not done yet. Remember the console.log statement in the fetchArticles function? Let's check out the console running our hosting server (i.e. where we've launched npm start):

Blog SSR Server Console

The message "Fetching articles..." has been yielded into the server console. This is another proof that getInitialProps is run server-side, and correspondingly the backend server requests for the SSR blog page are made by the hosting server.

As usual, I reload the SSR blog page 10 times on my local machine and fix the request timing. Here are the experimental timing values:

Blog SSR Chart

Avarage values (blog page - SSR):

  • DOMContentLoad - 1.059 s;
  • Load - 1.419 s;
  • Finish - 1.435 s.

Summary

We have tested out the SSG and SSR modes for a backend-fetching page. Let's now briefly summarize the features of each mode (which we have practically experienced).

The static site generation (SSG) mode yields:

  • faster initial page loading;
  • AJAX requests to backend server(-s);
  • need to implement loading state for component(-s);
  • AJAX-related side effects (e.g. need to enable CORS on backend servers).

On the other hand, with server-side rendering (SSR) mode comes:

  • the target page is content-ready;
  • no need of loading states;
  • slower initial page load due to additional or increased latency;
  • backend requests are made by the hosting server.

At this point a logical question arises: which one is better? The answer is predictable as well: it depends. Each mode has its advantages and drawbacks. Some use cases will encourage using the SSG, while for other tasks the SSR is much better.

And there are a lot of online materials highighting best use cases for SSG or SSR. And I'd prefer not to repeat them in this article. What I do want to say is that Next.js has been originally created for the server-side rendering (SSR) use cases.

You see, you don't really need Next.js for an SSG-style page that fetches backend, you can use any React framework for that (the create-react-app, for instance). But if you have chosen Next.js, chances are you want your pages to be rendered server-side.

Conclusion

You have read the second article of the Next.js SSG vs SSR series! We have explored the differences between the Next.js SSG and SSR modes to render a backend-fetching page via practical experiments.

Don't miss out next articles of the Next.js SSG vs SSR series, as well as articles about other coding topics. And don't hesitate to share your thoughts on Twitter about this article and the blog in general! Thanks for reading.

Photo by @glenncarstenspeters from Unsplash