Server Components vs. SSR in Next.js

Server Components vs. SSR in Next.js

The marginal difference between React Server Components and server-side rendering.

Foreword

I recently published an article, in which I discuss the core ideas behind React@18's Server Components. I'd say the article is one of the chunkier ones. It discusses the limitations of React's Client Components and how the Server Components help us mitigate these limitations.

Overall, Server Components are an exciting shift to the React (and Next.js) ecosystem. A next step, that'll enable us - the engineers - to benefit from a new concept with only a slight pivot to the APIs we're used to. Although, the other important question is how severe a mindset shift will the concept require?

Server Components are one more weapon in the already stacked arsenal of today's front-end engineers. Next.js already exposes SSR (Server-side rendering), SSG (Static-site generation), and ISR (Incremental static regeneration). Let's discuss, how RSC (React Server Components) fit into this arsenal and how they work together with the rest of the technologies.

Rendering strategies exposed by Next.js

Let's jump right into it, shall we?

SSR vs. RSC

The names and the abbreviations look similar. However, the idea behind both differs significantly. Moreover, you can even leverage both concepts in parallel!

Let's compare the two concepts for these aspects:

  • When to use them?

  • What is the output of each?

  • When is the output built?

  • What's the granularity?

  • How to use them in Next.js?

When to use them?

Should we use server-side rendering or a set of server components? Well, what about using both?

SSR

Let's start with SSR. A side note: Next.js uses the term "Dynamic pre-rendering" in place of SSR.

To describe SSR in layman's terms:

The purpose of SSR is to give the user something to look at before JS is executed.

SSR splits the rendering process into two parts:

  1. (Pre)rendering HTML. (server-side)

  2. Hydrating the HTML with JS to achieve interactivity. (client-side)

The HTML is dynamically pre-rendered on the server and then sent to the client along with JS bundle(s) and other assets.

This way, we can present the user with at least a non-interactive website (that usually looks pretty close to the final product) for that couple (hundred) milliseconds, that it takes the browser to load, parse and execute JS.

The motivation behind SSR should be obvious. It's always better to show the user something "tangible" rather than a blank screen. Or do you prefer this?

Spinner example

There are also other classes of benefits, such as better SEO. Not all of today's search engines execute JavaScript when generating page scores, right?

RSC

On the other hand, RSC (React Server Components) is a concept that allows us to decide per component, what to render on the server, and what to render on a client. There is no direct relationship between SSR and RSC. Taking words from the RFC, these two are not supposed to be antagonists, but rather complementary. Meaning, you can even use both simultaneously!

As for the usage, the rule of thumb is to use Server Components by default, over Client Components and use Clients Components only for:

  • Encapsulating interactive behavior

  • Interacting with browser APIs

If you're interested in why is it better to render your components on the server by default, you can read my other article or take a look at Next.js docs.

What is the output?

Server-side rending is an established pattern in the front-end world. The Server Components are still quite new and as for implementation quite interesting. Let's look into why!

SSR

This is quite intuitive. The output of SSR is HTML, which is generated on the server side. The markup does not have to be static, it can depend on the dynamic data available at the time of the request!

This allows us to transfer leaner chunks of data to the client and build more responsive pages on slower devices, resp. connections.

SSR chart

RSC

Here comes something new!

The intuitive train of thought here could be that since a React Server Component is just a component, that is rendered on the server, the output could be markup (the same as for server-side rendering), but just a smaller chunk, right? This is a perfectly reasonable train of thought and could as well work, however, this is not how server components work.

The reality is, that instead of a markup-type notation, the React team decided to go with a custom implementation for the RSC serialization format. The result is a custom JSON-like notation that is supposedly simpler to work with (internally) and suited better for transferring components over the network barrier.

Here's an example of a serialized server component.

M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":"@4"}]}]]}],["$","section","null",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":["$","div",null,{"className":"note--empty-state","children":["$","span",null,{"className":"note-text--empty-state","children":"Click a note on the left to view something! 🥺"}]}]}]}]]}]
M5:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J4:["$","ul",null,{"className":"notes-list","children":[["$","li","1",{"children":["$","@5",null,{"id":1,"title":"Meeting Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting Notes"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","2",{"children":["$","@5",null,{"id":2,"title":"A note with a very long title because sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this app! These note live on the server in the notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because sometimes you need more words"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","3",{"children":["$","@5",null,{"id":3,"title":"I wrote this note today","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note today"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","4",{"children":["$","@5",null,{"id":4,"title":"Make a thing","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It's very easy to make some words bold and other words italic with Markdown. You can even link to React's..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Make a thing"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","6",{"children":["$","@5",null,{"id":6,"title":"Test Noteeeeeeeasd","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Test note's text"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Test Noteeeeeeeasd"}],["$","small",null,{"children":"11/29/22"}]]}]}]}],["$","li","7",{"children":["$","@5",null,{"id":7,"title":"asdasdasd","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"asdasdasd"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"asdasdasd"}],["$","small",null,{"children":"11/29/22"}]]}]}]}]]}]

To some extent, the format is quite readable, we can see that our component consists of:

  • Two atoms - a div and an ul. These are Server Components, pre-rendered on the server!

  • A suspense boundary. Not so interesting in this context.

  • Two pointers to a Client Component (SearchField and EditButton). These will be rendered on the client.

Here's how could update the diagram from above to reflect the server component architecture. Notice that Server Components can be streamed to the client in parts.

RSC chart

When is the output built?

We discussed what the output looks like. We now know that SSR returns markup, but RSC interestingly enough returns a custom JSON-like notation! Both are obviously built on the server, but each one of them is built in a different part of the flow.

SSR

If you paid attention, we already touched on this topic in the previous paragraphs. The SSR-produced markup is produced at the requested time. This is the reason, why SSR is also called "dynamic pre-rendering", as opposed to "static pre-rendering" (aka. SSG), which produces the markup statically, at build-time.

The flow goes:

  1. The client requests a page from the server.

  2. The server pre-renders HTML and bundles JS.

  3. The assets are passed to the client.

  4. The static markup is rendered.

  5. The static markup is hydrated with JS.

SSR time chart

RSC

The server components are more asynchronous and streamable. By design, they are also more granular (we'll touch on this more in the upcoming paragraph), so when there is an UI interaction, that requires a component on the server side, the component is requested by the client, build dynamically on the server and streamed back to the client as an atomic chunk.

If we combine the flow with server-side rendering, we end up with:

  1. The client requests the output from the server.

  2. The server pre-renders HTML and bundles JS.

  3. The assets are passed to the client.

  4. The static markup is rendered.

  5. The static markup is hydrated with JS (state and event handlers attached to the DOM).

  6. An UI interaction happens.

  7. Client requests the relevant RSCs to from the server.

  8. RSCs rendered and serialized on the server and passed to the client.

  9. RSCs are hydrated into the current web page.

RSC time chart

Note: The initial chunk can also contain server components. I chose to demonstrate this in an example, where the components are rendered as a result of user interaction with the page.

What's the granularity?

How granular are the SSR and RSC outputs? This is a straightforward one.

SSR

We can say that the granularity of SSR is 1. By this, we mean that for 1 page, there is only 1 request/response pair that returns the initial server-rendered markup.

After the initial portion of markup is rendered, the SSR served its purpose and is no longer needed.

SSR has granularity of 1

RSC

Oppositely to SSR, the granularity of RSC >= N, where N is the number of server components used on the page. I'm including the >= operator, as one component may be rendered multiple times. Hopefully, it's cached.

The RSCs allow us to render little pieces of the page on the server and stream them to the client in small chunks, which brings many benefits. The more chunks we decide to defer to the server, the more granularity we introduce. The point is, there is no upper limit to this.

SSR has granularity of >=N

How to use them in Next.js?

Let's talk practice. How do you even trigger SSR or render a Server Component in Next.js? At the time of writing this article, no other frameworks support Server Components out of the box. Of course, you can still configure your server to handle them!

SSR

Next.js is quite an advanced framework, that pre-renders every page by default. Moreover, it lets you choose, whether you want to pre-render statically (during build time) or dynamically (during request time).

  • Use getStaticProps and getStaticPaths for SSG (default)

  • Use getServerSideProps for SSR

Example of getServerSideProps from Next.js docs

RSC

As mentioned before, Next.js takes a stance of treating every component as a Server Component by default. If you want to use a Client Component, you'll need to annotate the file with use client; directive at the top of the component file.

Also, don't forget that the concept is only available in Next.js@13 app directory. You cannot leverage it inside the good old pages directory.

Example of a client component takes from Next.js docs

Conclusion

Opposed to my previous article which attempts to discuss the benefits of server components in-depth, the idea of this article was to provide answers to quite basic questions that may pop up to a developer who is new to the concept.

Hopefully, I hit the nail on the head at least partially and the article did help you grasp the concept a bit better.

For more articles like this visit Webscope's Blog! We publish two practical technical articles per month.

If you'd like to catch up with me, I'm active daily on Twitter.