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 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.
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:
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. 💪🏻
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.
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.
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.
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:
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.
We introduced today the idea behind the new experimental Server Components and Shared Components.
Let’s recap:
Since I really enjoyed the Data Fetching with React Server Components" talk – I’m attaching here all parts of the video by chapters. 🍿