Motivation
In software development, the "Fast/Good/Cheap Rule" term refers to compromises we need to do on one of the speed-quality-efficiency trio – in order to maximize the rest of them.
When we develop components, the rule is reflected and becomes a concrete challenge – how could we create components that are simple with maintainable code, producing a good user-experience, while having fast performance as much as possible? Indeed, according to the rule, it’s not possible.
But maybe, there is a way to leverage the server-side to solve the performance problems by default, while still allowing achieving interactive and rich user-experience with sort of the same ecosystem, though? 👀
Well, for that purpose, the React core and data teams recently researched and created an RFC proposing new types of components "Server Components" and "Shared Components" aiming to beat the challenge. They also did an informative talk (all chapters are attached below) and created an example project – check them out as well.
Let’s introduce those components!
⚠️ Note: This proposal isn’t production-ready and still under experiment – which means, we shouldn’t use it for production. However, we more than welcome to play with that and provide to the React teams our feedbacks.
The content is available as a video as well:
Server Components
Server Components are a new sort of React components which simply run only on the server-side, and so primarily allowing to avoid downloading their code to the client-side:
import MyClientComponent from 'MyComponent.client';
function MyServerComponent(props) {
// For instance, the data could be fetched directly from a database
const { data } = props;
return <MyClientComponent data={data} />;
}
They seem like the traditional React components (which from now on, are named “Client Components”), hence in order to distinguish – Server Components cannot manage state, use rendering lifecycle (useEffect
/useLayoutEffect
), access browser’s APIs or any custom functionality that depends on those. Also, notice the naming conventions – .client.js
and .server.js
for Client and Server Components respectively.
However, as stated, they are meant to complete the ecosystem and work together with Client Components and SSR – so that we have the interactivity of client-side application but with a better performance using the server-side. The interesting thing is, that placing components on the server-side yields a couple of benefits in addition to the fact that their code isn’t downloaded.
Obviously, there are drawbacks – whether it’s a new standard of components to learn, specific constraints to make them work, the new conventions might seem confusing and messy and probably more. Having said that, with a successful RFC, the team (and the community) might minimize the drawbacks and intensify the benefits.
So speaking of the benefits, let’s explain them.
Zero Bundle Size
SSR is a technique to quickly render client-side applications on the server thereby returning a complete HTML markup, which was rendered in advance, to the client. That is, in SSR we use the server-side to output pre-made HTML, thereby improve the loading performance. Yet, it doesn’t deal with:
- JavaScript Bundle – SSR merely uses the bundle to render the pre-made HTML without affecting it
- Interactivity – SSR is mainly being used for a non-interactive initial render while the components are hydrated
Which begs the question, why not having a technique completing SSR and reducing the JS bundle size while allowing interactivity?
Indeed, Server Components were born exactly to fulfill this purpose.
Previously, we mentioned that they allow avoiding downloading their code to the client-side. So, instead of adding them to the JS bundle and letting the client render them – they are rendered statically on the server (the render stops at native elements and Client Components) and streamed in a format ("Virtual DOM") allowing reconciling seamlessly the server-side tree with the client-side tree:
{{< figure src="../../images/posts/2021-01-09-introducing-server-and-shared-components-in-react/server-component-rendered-result.png" width="600" class="shadow" title="Server Component’s Rendered Result" alt="Server Component’s Rendered Result" >}}
The magic happens since the client regularly renders the native elements and Client Components, while progressively receives and paints the streamed response. The important thing to understand here, is that the dependencies the Server Components use stay on the server-side and aren’t part of the bundle at all.
In this way, Server Components (and the dependencies they use) have a nonexistent impact on the bundle size and by using them – we can reduce the overall bundle size significantly. ⚡
But we also said that Server Components allow interactivity, which doesn’t completely get along with the fact they are rendered statically.
The truth is, that Server Components will arrive with a framework (such as Next.js) supporting a refetching mechanism, which allows the client-side to request a rendered result of a Server Component. Meaning, the client-side makes a request for refetching a specific piece of UI using the framework, and when the framework on the server-side receives that, it makes React on the server-side rerender the Server Component and similarly the framework streams the rendered result progressively to the client-side. 💪🏻
Full Back-End Access
Another benefit of having the components on the server-side, is accessing its data and files easily.
For example, we can directly read from the database and pass it to the client component – without performing any request to fetch the data:
import db from 'db.server';
import MyClientComponent from 'MyComponent.client';
function MyServerComponent(props) {
const { id } = props;
const dataById = db.data.get(id);
return <MyClientComponent data={dataById} />;
}
It might help to remove the request latency and improve the performance, since the client-side doesn’t need to make sequential requests to fetch the data, but rather to have it directly inside the Server Component by the need.
Of course, this only to demonstrate competence – in the same manner, we could read from the file-system, access microservices and so on.
Automatic Code Splitting
Code splitting is a technique to split the bundle into multiple smaller bundles, that will be loaded dynamically and lazily at runtime, and so improve dramatically the performance. This ability is made possible through dynamic imports, React.lazy
and supported bundlers (for example – Webpack, Rollup, etc.).
The main drawback of this technique is that we need to do it manually, meaning, splitting the code into bundles in order to load it lazily based on some logic:
const FirstComponent = React.lazy(() => import('./FirstComponent.client.js'));
const SecondComponent = React.lazy(() => import('./SecondComponent.client.js'));
function MyClientComponent(props) {
const shouldDisplaySecondComponent = ...; // Some logic
return shouldDisplaySecondComponent ? <SecondComponent/> : <FirstComponent/>;
}
Contrarily, the beauty of Server Components is that this bundle-separation happens automatically:
import FirstComponent from './FirstComponent.client.js';
import SecondComponent from './SecondComponent.client.js';
function MyServerComponent(props) {
const shouldDisplaySecondComponent = ...; // Some logic
return shouldDisplaySecondComponent ? <SecondComponent/> : <FirstComponent/>;
}
As we already said, the client-side only "downloads" the rendered result without any impact on the JS bundle.
The Same Syntax
Typically, when developing client-side and server-side, we tackle different challenges which demand different technologies and solutions.
The power of Server Components is bringing the ability to mix (for better or worse) and use a single ecosystem with the same language for both the client-side and server-side. Although it might arrive with a little learning-curve and magical interactivity mechanism, it’s still React.
Shared Components
Sometimes, we might want to create a component working both on client-side and server-side – so that the same logic and behavior are shared between both environments.
The RFC introduces a pretty good example in order to explain the need – a MarkdownRenderer component:
- On client-side – the component is needed when the user wants to edit the content, and the MarkdownRenderer component can be downloaded and provide a live preview while the user edits
- On server-side – the component is needed to render a viewer displaying the content that was written in markdown
That sounds great, so theoretically why not just developing Shared Components? 🤔
The thing is, there are no free rides, and Shared Components have all constraints of Server Components (no state/rendering lifecycle hooks/browser APIs) and Client Components as well (which is merely not accessing the server-side resources).
That being said, Shared Component still might be handy for certain use-cases, as we already explained with the MarkdownRenderer component.
Summary
We introduced today the idea behind the new experimental Server Components and Shared Components.
Let’s recap:
- As software developers, we’d like to strive for an easy and maintainable code, producing a good user-experience, while having fast performance as much as possible
- Server Components are run on the server-side only and so their code isn’t downloaded to the client-side
- Server Components cannot manage state, use rendering lifecycle or access browser’s APIs
- Server Components are rendered on the server-side which means they have zero bundle size
- Server Components are returned to the client-side as a virtual DOM format of the server tree ready to be reconciled on the client-side
- Server Components will support refetching mechanism to be rerendered when the client-side triggers so
- Server Components have full access to the server resources – such as the file-system or a database
- Server Components brings automatic code splitting by default without doing anything manually
- Server Components make a single ecosystem for both client-side and server-side
- Shared Components are components that can run on both client-side and server-side
- Shared Components should follow the constraints of Client Components and Server Components
The Talk
Since I really enjoyed the Data Fetching with React Server Components" talk – I’m attaching here all parts of the video by chapters. 🍿