In this article, we explain the concepts of CSR, SSR, SSG, ISR, RSC, SPA, Streaming, and Navigation in the context of a Next.js project, helping you gain a clear understanding of Next.js.
SPA
Let's kick things off with something we're all familiar with: SPAs. According to MDN, here's the lowdown on SPAs:
An SPA (Single-page application) is a web app implementation that loads only a single web document, and then updates the body content of that single document via JavaScript APIs such as Fetch when different content is to be shown.
This therefore allows users to use websites without loading whole new pages from the server, which can result in performance gains and a more dynamic experience, with some tradeoff disadvantages such as SEO, more effort required to maintain state, implement navigation, and do meaningful performance monitoring.
So here's the question: Are projects created with frameworks like Next.js or SvelteKit considered SPA? Let's take a look at a Next.js project to find out.
This is the official website of Next.js. When you click on a link, the page doesn't refresh, but the main content does get updated, and the route address changes. So, is this kind of project a SPA?
It's hard to say and can be controversial.
If you go by MDN's definition of "loading a single web page and then updating the main content with JavaScript," then it's considered a SPA. But usually, people think of SPAs as having pure client-side rendering and not updating the URL when the content updates (which is why SPAs have SEO issues).
However, Next.js projects both update the URL and use SSR, which makes up for the SEO issues that SPAs have. In fact, neither the Next.js nor the Svelte official websites use the term SPA directly when introducing themselves; instead, they use words like "Navigating" to describe content updates through JavaScript.
So, it both is and isn't. When it is, the term traditional SPA is also used to make a distinction.
Navigation
How is an SPA implemented? That brings us to the concept of Navigation, which is essentially the process of moving from one URL to another.
In the case of the Next.js official website, Navigation primarily refers to client-side navigation. This is where JavaScript intercepts the link clicks, fetches the address of the target route, and then updates the route accordingly.
CSR
CSR, which stands for "Client-side Rendering". As the name suggests, the rendering work is mainly carried out on the client side.
The traditional way we use React is an example of client-side rendering. The browser first downloads a minimal HTML file along with the necessary JavaScript files. Within the JavaScript, operations such as sending requests, fetching data, updating the DOM, and rendering the page are executed.
There are mainly two issues with CSR:
Performance issues, as rendering on the client side is subject to the client's environment, such as internet speed and device performance. The page will not be fully rendered until the JavaScript is downloaded, parsed, executed, and the data requests are returned.
-
SEO issues, although modern crawlers are generally capable of supporting pages rendered with CSR, the level of support varies.
SSR
SSR, which stands for "Server-side Rendering," is a method where the rendering of web pages is done primarily on the server. Here's a translation of the provided text into conversational English:
SSR, short for "Server-side Rendering," is all about doing the heavy lifting of rendering on the server side. Imagine loading a blog post page; instead of making the client handle the request every time, which might be slow due to their internet connection, the server can take the initiative. It can directly fetch the data from the API, render it into a static HTML file, and then send that back to the user.
Even though it's still making a request, the server typically has a more robust environment in terms of network and hardware, which usually results in faster rendering times, especially for the first screen load.
While the overall speed is often faster, SSR might take longer to respond compared to CSR. This is because SSR has to request the data, render the HTML, and then send it back. In terms of performance metrics, specifically TTFB (Time To First Byte), SSR takes more time. That's why in development, you can't just throw all the APIs into SSR; doing so could actually make things slower.
On the plus side, SSR is great for SEO. It solves many of the SEO issues that can come up with client-side rendering.
SSG
SSG, which stands for "Static Site Generation". Compiles pages into static HTML files during the build phase.
For example, when opening a blog post page, since the content displayed to everyone is the same, there's no need for the server to make a request to an API every time a user requests the page. Instead, the data can be fetched beforehand and compiled into an HTML file in advance. When the user visits the page, the HTML file is served directly, resulting in faster loading speeds. Additionally, pairing this with CDN caching can make it even faster.
ISR
ISR, which stands for "Incremental Static Regeneration". Imagine you're opening a blog post page: the main content might stay the same, but things like likes and bookmarks are always changing. With SSG, once the HTML file is generated, these dynamic data points can't be updated accurately, so you might end up switching to SSR or CSR.
To address this, Next.js introduced ISR. When someone visits the page, they see the old HTML content at first. Meanwhile, Next.js generates a new static HTML file in the background. The next time you or another user visits the page, it’ll show the updated content.
Streaming SSR
Traditional SSR, while solving SEO issues, still has a lot of problems. First, SSR needs to fully render the page on the server before sending it to the client. Second, to keep the server and client component trees consistent, all the component code has to be bundled into the client-side package. Lastly, once hydration starts, the entire process is blocking, meaning the user can’t interact with the page until hydration is fully complete.
To address these issues, Streaming SSR, or what’s commonly known as "streaming rendering," was introduced. Next.js uses HTTP's chunked transfer encoding to implement this. With chunked transfer encoding, data is sent in a series of chunks from the server to the client, allowing the page to be progressively rendered as it’s received.
In simple terms, the server keeps sending content to the browser, and the browser renders it as it comes in. React uses the Suspense component to make this work. Suspense cleverly uses a placeholder while the data is loading, and once the data is ready, it streams the real content into the HTML, replacing the placeholder and allowing for progressive rendering.
This approach solves two main issues:
- Components don’t have to be fully rendered on the server before being sent to the client—they can be streamed progressively, which improves the user experience.
- Selective hydration: the page can be hydrated in parts, and even prioritize hydration based on user interactions.
However, it doesn’t completely solve all the problems. The client still has to download all the component code and hydrate everything, even if some components only need static rendering and don’t require any client-side interaction, which wastes performance. The current ultimate solution to this is React Server Components (RSC).
RSC
RSC, which stands for "React Server Component," introduces a new way to think about rendering pages. Previously, concepts like SSR (Server-Side Rendering), CSR (Client-Side Rendering), SSG (Static Site Generation), and ISR (Incremental Static Regeneration) applied to entire pages, meaning the whole page had to be rendered using one method. But let’s rethink how we want to render a page.
Take a blog post page, for example. It has purely static parts, like the article content, and interactive parts, like the like and bookmark buttons. If we break it down by components, we can define the static parts as server components. Why server components? Because they render faster on the server, are SEO-friendly, and produce HTML + CSS, which is perfect for static content.
On the other hand, the interactive parts are client components because they require client-side interaction, meaning they need browser DOM events. This requires hydration (the process of adding event handlers) on the client side after rendering.
Since server and client components are handled differently, they need to be distinguished. Client components are marked with a use client
directive. Once you break down a page into multiple server and client components, it becomes difficult to label the entire page as SSR or CSR. This is why, after Next.js 13 introduced the App Router, the official documentation de-emphasized these terms. If you still want to think in those terms, you could simplify it by saying that server components are SSR and client components are CSR.
Now that the components are split, how do we render the page? Server components are straightforward—they’re rendered on the server as HTML + CSS. Client components also go through an SSR process initially because they might return some content, but they’re marked as client components (a process some refer to as "marking holes"). The libraries and code dependencies for server components stay on the server, while the client components’ dependencies are bundled and sent to the client, where hydration and rendering take place.
In simple terms, all components initially go through SSR. The client components are marked, and the output is HTML + CSS, while the client components’ dependencies are bundled into JS. Both the HTML and JS are sent to the client, where the JS runs and hydrates the client components, adding the necessary interactive events, thus completing the initial page rendering.
I believe RSC solves two major problems:
- Bundle Size Reduction: By splitting components into server and client components, server components are rendered on the server, and the client only needs the final rendering result. This means the server component dependencies don’t need to be bundled into the client JS, reducing the size of the client bundle.
- Selective Rendering and Hydration: In traditional SSR, all component code had to be downloaded to the client for hydration. But with RSC, since components are clearly differentiated, only the client components need hydration.
When navigating to a new page, instead of fetching data on the client side like in traditional CSR, RSC handles the rendering on the server. Directly replacing the current HTML with the target route’s HTML would break the page’s state, so RSC uses a custom format called RSC Payload. This payload includes the rendered result of the server components, placeholders for client components, references to files, and the data passed from server components to client components. The client then uses this payload for partial rendering and updates, allowing the state to be maintained.
Summary
RSC doesn't conflict with SSR or Streaming; it's actually a blend of these technologies in Next.js. For instance, all components, whether client or server, are rendered on the server, and the HTML is streamed to the client. During navigation, the RSC Payload is optimized for streaming too. RSC and SSR complement each other, working together to boost app performance.