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.
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:
- (Pre)rendering HTML. (server-side)
- 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?
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.
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 anul
. 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
andEditButton
). 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.
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:
- The client requests a page from the server.
- The server pre-renders HTML and bundles JS.
- The assets are passed to the client.
- The static markup is rendered.
- The static markup is hydrated with JS.
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:
- The client requests the output from the server.
- The server pre-renders HTML and bundles JS.
- The assets are passed to the client.
- The static markup is rendered.
- The static markup is hydrated with JS (state and event handlers attached to the DOM).
- ✨ An UI interaction happens. ✨
- Client requests the relevant RSCs to from the server.
- RSCs rendered and serialized on the server and passed to the client.
- RSCs are hydrated into the current web page.
Note: The initial chunk can also contain server components. I chose to demonstrate this on an example, where the components are rendered as a result of an 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.
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.
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
andgetStaticPaths
for SSG (default) - Use
getServerSideProps
for SSR
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.
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.
Top comments (3)
Thanks for sharing. This is a great Article.
Thanks for reading, Femi! I appreciate it.
Thanks a lot! Finally I had a chance to understand the changes ;)