Disclaimer: I have done no profiling / testing regarding any of the following. I'm also not an expert on RSC and this is all from a pretty naive point of view.
tl;dr Code is actually a pretty good wire format over the long run. RSC doesn't eliminate loading, but rather trades loading code for loading markup (an implicit cost that many developers will not reason about). Most code is not expensive, and of the code that is, it's often client-side code. If you mix dynamic and static markup you might have a lot of redundant fetching. Given this and other common pitfalls of server component, might it make sense to have users manually apply it to specific pieces of code, where they know what they're doing?
To make the transition to Server Components easier, all components inside the App Router are Server Components by default, including special files and colocated components. This allows you to automatically adopt them with no extra work, and achieve great performance out of the box.
"no extra work" and "make the transition to Server Components easier" doesn't seem to have panned out for many, and in this post I will question "achieve great performance out of the box".
RSC Use-Cases
RSC is a very useful feature and brings incredible DX to full-stack, but it seems to be tripping up a lot of new developers who are used to React being client-side code by default (and perhaps the server/client boundary is now too seamless - a good thing but also a potential source of confusion for beginners). And I suspect you'd need much deeper tech than what we have currently for warning people that a component should be "use client" (static analysis or such).
My assumption for why "use client" is not the default (which would be more accessible), is that server component is viewed as a win by default (from the Next.js docs: "Moving client components to the leaves" - a principle which I'll question in this post - and which also contradicts React's vision of interleaving). I think RSC makes sense for:
- when the markup is smaller than the data
- when fetching the data involves a waterfall
- when the libraries used to render the markup are very large and/or computationally expensive (e.g.
<Markdown />is a good use-case!) - when the markup never changes and can be cached indefinitely
- when you want to use secrets / Node.js APIs directly in React instead of writing an API route (this doesn't actually eliminate network hops, it just maybe eliminates some code / eliminates backend eng in favor of "frontend" eng learning SQL/etc)
Most well-designed APIs shouldn't cause issues 1./2. (maybe they are issues if you like, don't paginate your request), and you could count React.lazy / next/dynamic as part of a waterfall, but I suspect people don't have a lot of React.lazy + fetch waterfalls, and lazy bundles for page transitions are prefetched already. w.r.t. 3., it seems like most people are using libraries mostly for client-side (and in many cases, 0 server-side libraries), which is probably where a lot of the new developer friction comes from. 4./5. are also not really reasons to make all components server components by default.
RSC Trade-Offs
There's a bandwidth tradeoff discussed in this Remix blog post. The article is really outdated in general, but this particular point seems like a fundamental limitation of information theory. The trade-off is you can download and cache a bundle upfront so you can generate markup from smaller recurring payloads, instead of downloading slightly smaller bundle but larger recurring payloads (I realize that server components can also be cached and are actually cached indefinitely by default in Next.js, but if you have a large JSX payload and there is a dynamic fetch or like 1 prop changes then it won't be cached I assume?). I suspect "use client" may be this tradeoff and may actually be preferred in many scenarios, especially with prefetched client bundles. In other words, I suspect "use client" may make better use of caching (we know which part is static - the JS) than server component where dynamic and static/deterministic components may be intermingled*.
*This is pure speculation - would be useful to have an in-depth guide here. If this is solved, i.e. static subtrees of server components can be cached, it changes the game completely. Fully static component trees are already a great use-case for server components of course.
The user experience of spending an hour reading status updates, updating records, creating posts, and sending messages on a low power device with a spotty network is just as important as the initial page load on any device on any network
SSR/SSG is generally a "free win" (other than the cost of "double data") since the initial HTML doesn't need React + Next, which are quite large bundles. But with RSC, you need to download and execute React anyway to render the server component. And the elimination of a one-time download of const a = () => createElement("div",...) via a 1+ time download of ["$","div",...] is a less convincing trade-off than eliminating JS entirely from the first contentful paint.
In RSC you don't have the problem of getting data onto the first page, as it's already there due to SSR/SSG. So you're only looking at time-to-interactive, which indeed I don't doubt server component marginally improves especially if the "saved" bundle size is much larger than the framework + client bundle size. But on subsequent pages, RSC might actually often be marginally worse at getting data onto the page? In pages/ router I have generally not had a client-side navigation not just be instant + 1 small API response after navigation.
An example of where server component seems like not a performance win is in a list or table component; anything that uses .map or something for which the output could be even larger than the bundle itself. But it seems tricky for a new developer to think about this and consciously opt-out of server component in such cases.
The Question
I worry that server component could be a React.memo/PureComponent/useMemo/useCallback type of situation where an assumption proliferates that it's always an optimization, when that may not be the case some or even most of the time (and then Facebook looks into making a compiler for inserting "use client" vs server component automatically or whatnot?).
Maybe the best practice becomes to isolate small snippets of code which are especially taxing to the client and add server component to only those. I'm completely on board with importing server components off npm for example. In particular, adding a server component is an alternative to adding an API route (an intentional and surgical operation) - i.e. you can always substitute one for the other - where adding a server component is more convenient but also easier to mess up. With API routes you have to be explicit about what you're sending over the wire, but with server components it's more implicit and will generally be more than necessary if gets default applied to most of your tree. See here for an example where client components would not only have a higher ceiling for optimization but also push a developer to pursue it. (This is actually a good illustration of my point because it's the kind of page people build all the time and many people are going to just follow Next.js's guidance to "move client components to the leaves" when actually you still need to think carefully about / optimize your client/server boundary just like you did with API routes)
One could argue, the cases in which server component is accidentally a performance downgrade is safer to ignore than the cases in which client components are a big performance downgrade, even if the former turn out to happen more often. But from a support/code review POV, I can easily imagine the number of questions to which the answer is "shoulda "use client"" will far exceed that of "shoulda used server component" in the coming years.
And the hurdle that server component needs to pass for it to be a good default is not just that it performs better, but that it performs so much better that it's worth tripping up new developers (unless the point is to provide a steep learning experience upfront).
Here's a half-baked alternative I can come up with on the spot:
- Either make components client components by default, or force the user to explicitly choose server or client
- If a component is static (no dynamic fetches, no props), recommend that it be a server component
- If a component is analyzed to not be runnable on the server, recommend that it be a client component, and vice versa (things that are already being done, and can get arbitrarily advanced)
- Recommend that users of server components check the Network tab to see what props/markup are being spit out (and maybe even have smarter tooling around this), i.e. to treat the component as if it were an API, because it is
Again, I haven't run any numbers and I'm sure the React/Next.js teams have analyzed this much more thoroughly (I'd love to see a post about this though!), so what I'm saying could be really dumb. Thanks for reading anyway.
Could you please make a precision with what do you mean with "use server", the use server out there at the moment is only for server actions.
Components are opted into Server Components, RSC, by default, unless use client is present, or if they are part of a use client import tree.
I am worried that people will start to think that use server is the opt-in to Server Component, when it is not. As of today use server opts in to server actions:
When using a top-level "use server" directive, all exports below will be considered Server Actions. You can have multiple Server Actions in a single file.
4 replies
Thanks! Sorry for being annoying like that, but this kind of discussion needs exact wording. I've seen far too much tech-influencer content that throws terms around without any care, and people who listen to that, often end up in Reddit or here, with very confused metal models.
No worries, thanks for the correction!
One thing about that video is that the API response is not optimized, i.e. it loads a lot of unnecessary fields (this is easily fixed with GraphQL but also not hard in REST, with query params). And so when he prints:
http://localhost:3000/aggregated-client
HTML: 60.60 KB
JSON: 96.74 KB
JSON Client Data: 75.26 KB
http://localhost:3000/aggregated
HTML: 60.60 KB
JSON: 73.38 KB
JSON Client Data: 0.00 KB
I think that the 75.26 KB "JSON Client Data" number (and thus the 96.74 KB "JSON" number for the "use client" version) could be significantly smaller - basically if you look at 16:08 and compare it to what's actually needed by PhoneCard at 21:09, 90% or more of the data is unnecessary for his app and can be removed. So that JSON could probably be <30 KB. Also one trick you can do if you have no control over the API response, is to remove data after fetching but before passing to the client component. Like:
const _iphones = await fetch("http://localhost:3000/iphones")
const iphones = _iphones.map(justTheDataMyComponentNeeds)
return <Phones phones={iphones} />
This also removes data from the RSC payload.
Meanwhile the 73.38 KB from the server-side is from all the redundant HTML, tailwind classNames etc which can't be removed (fundamental to how RSC works). Over time if the user keeps clicking back to this page it'll add up because that 73.38 KB needs to be fetched every time if the cache has expired (60 seconds).
So in this particular case server components was indeed less data, but the question is still open:
- Is it less data in close to 100% of apps? If not, what justifies having it on by default for 100% of apps and components? Or should it be something users opt into after profiling?
- If it's less data because of an inefficiency in the request/response, is it the right move to silently wrap everything in server component instead of exposing the root cause? (this question is a bit like the React question of - do you solve the root cause of inefficiencies in your React app first or do you slap
React.memofirst)
Hello @llllvvuu i just came accross this discussion not long ago, but i think you are looking too far into why RSC are the default.
The simpler explanation is that RSC are the default because the server is the first entry in your app. when you make a request to your app, the first thing that is hit is your server, then after the server has returned a response, the client code then start to execute (Hydration & such). so having server component as the default let you decide what you want to send to the client and makes it possible to optimize the response you want to send in the client, one simple big win for server components is deciding just what components to send over the wire and what data to send.
Take an app which is translated into multiple languages, being in the server let you decide to send either only the dictionnary for the requested language to the client or can even let you decide to send only the strings required for the page in the client. Don't listen to me, but see this article that just outlined that : https://www.smashingmagazine.com/2023/03/internationalization-nextjs-13-react-server-components/ . This is a big win as languages dictionnaires can be ginormous in size for bigger apps.
If you want another example, take an app that needs to do A/B testing and render a different UI. With RSC you can choose to render the correct UI on individual components without any client side waterfall.
I pre-shot the question : "these two examples could be done in client components, in fact they are done with client components today, so why RSC ?"
the simple answer : with these components being in the server you can avoid having Layout shift on the client and eliminate waterfalls.
- in the case of the app needing translations, you can decide to load your dictionnary in a context and apply lazy loading on the client, but you get a waterfall and possibly opt out of SSR. You could load the data in
{getServerSide,getStatic}Propsbut you'd have to send the whole dictionnary for the language over the wire. - in the case of A/B testing, you'd have to load all the components needed to render every version of your app and decide on the client which to render, if your app is statically generated, you may even have your app render the statically generated UI on the first load and have the UI flashing to the correct one after it has fetched the data to decide which UI to render.
Another reason that RSC are the default and you can't import server components from client ones is that if it was the case, you'd have very bad waterfalls all over your app, because what would happen in the state on the client component changes, does the server component get refetched ?
Take this example :
"use client" import * as React from 'react'; import ServerComponent from '~/components/server-component' export function ClientComponent() { const [counter, setCounter] = React.useState(0) return ( <> <button onClick={() => setCounter(counter + 1)}>{counter}</button> <ServerComponent counter={counter} /> </> ); }
With this, everytime the component would be rendered it would issue a refetch to get the new server component. Now think about it : if the request to your server is very slow, what would be rendered here ? empty markup ? should it wait for the server component to finish fetching to show anything ? We can both agree to say it would not be ideal right ?
Now what if the component visibility was controlled by a parent :
"use client" import * as React from 'react'; import { ClientComponent } from '~/components/client-component' function ParentClientComponent() { const [isShown, setIsShown] = React.useState(true) return ( <> <button onClick={() => setIsShown(!isShown)}>{isShown ? "Hide" : "Show"} <button> {isShown && <ClientComponent /> } </> ); }
Everytime the isShown state would be set to true, the client component would have to issue a refetch, because that's how react works, the client component would be remounted, its state reset and all children rerendered.
This would be the behavior if client components were the default i.e the entry of your app.
How is the mental model to server components that i see : you start your page with the server, you can do everything you want in there, fetch data, render huge markup libs, etc. everything is static (as in not interactive) by default, and then when you need interaction (useState) you create a client component ("use client"). You can have it at the leaves for small buttons for ex. Or you can have the need of interactions very high within the tree, both are fine, that depends on you, use the amount of client components you find necessary for your use-case.
If your app is heavily interactive you will need to have many client components anyway.
Where i think RSC are pretty cool and very usefull IMO is for data-fetching, having it on the server makes it possible to make API calls while keeping your secrets "secret", and with that eliminate layout-shifts while working perfectly with SEO.
I think dan abramov have said somewhere on twitter that "use client" is fine and is not an anti pattern.
3 replies
Right, I acknowledged these use cases:
- when the markup is smaller than the data
- when fetching the data involves a waterfall
- when the libraries used to render the markup are very large and/or computationally expensive (e.g. is a good use-case!)
- when the markup never changes and can be cached indefinitely
- when you want to use secrets / Node.js APIs directly in React instead of writing an API route (this doesn't actually eliminate network hops, it just maybe eliminates some code / eliminates backend eng in favor of "frontend" eng learning SQL/etc)
but what I'm not convinced by is the idea that use client is only for interactive components. If you have:
<Fetch>
<Items>
<Item>
<Button />
Then obviously <Button> needs to be client component and you can make a good case for <Fetch> to be a server component (this is actually not a no-brainer though; we know that edge<>server is faster than client<>server but is client<>edge<>server necessarily faster?).
But my question is not about <Fetch> and <Button>. Past versions of Next.js already did these correctly, with getServerSideProps, so RSC is not about that IMO.
Rather, what RSC affects that server-side page props don't already affect are the components in the middle, e.g. the <Items> and <Item>.
I think Remix and later RSC correctly realized that sometimes you do want to make some of the middle components server-side. I think this is brilliant. Being able to give server-side props to any component is very powerful. But I also think most people don't need it. And it's very easy to introduce a performance regression if <Fetch>, <Items>, and <Item> are all server-side, if you don't realize that this sends props over the wire for every single <Button /> (in addition to all the redundant JSON that it sends for <Items>). getServerSideProps is much more explicit about this.
If what we actually want is just <Fetch> to be server-side, then shouldn't the defaults be designed differently?
in the case of A/B testing, you'd have to load all the components needed to render every version of your app and decide on the client which to render,
Only in a poorly engineered app would this be the case. IMO this use case actually favors explicit getServerSideProps; with implicit server components you enable devs to keep inefficiencies in their apps by sweeping them to the backend (more discussion here).
This would be the behavior if client components were the default i.e the entry of your app.
This isn't an issue with client components, but with server components. Thankfully, React disallows this from happening. So if client components were the default, then whenever you wanted to make a server component, React would force you to make all of the importing components server components as well, which is not an issue.
But with server components as the default, something like this actually can and will happen. Every <Link> unmounts and remounts server components, which can be a lot if you put the whole app into a server component (as opposed to just the fetching).
tl;dr Prior to RSC, Next.js already made the top-level server-side, and that was good. What Remix, or RSC without Next, allows is to inject server-side props into more places, which is also good. What Next.js defaults are pushing now is to put almost the entire app (everything except for interactive "leaves") into RSC, which seems like going too far / maybe a mis-use of RSC. It's not even clear if this is what RSC's design goal is.
Then obviously needs to be client component and you can make a good case for to be a server component (this is actually not a no-brainer though; we know that edge<>server is faster than client<>server but is client<>edge<>server necessarily faster?).
I don't understand ? 🤔
If what we actually want is just
<Fetch>to be server-side, then shouldn't the defaults be designed differently?
No need to change the defaults, just add "use client" on Items then what you have is that only <Fetch> is a Server component here, and since all the imports from a client component are considered as client components themselves, Items and its children become client components.
Only in a poorly engineered app would this be the case. IMO this use case actually favors explicit getServerSideProps; with implicit server components you enable devs to keep inefficiencies in their apps by sweeping them to the backend (#52119 (reply in thread)).
The thing here is about loading the bundle necessary to render your different UI versions, by default with client components, it would have to load all the versions on the client, even though they are not used, this is unavoidable as all the client imports are bundled, while importing a bundle into a server component only sends that bundle if it is rendered.
This isn't an issue with client components, but with server components. Thankfully, React disallows this from happening. So if client components were the default, then whenever you wanted to make a server component, React would force you to make all of the importing components server components as well, which is not an issue.
If you say that client components are the default and using server components means you send them to the root, it becomes just the same design as today. Because you'd have to tell at some point that the imported component is a client one right ? So you'd have to add a "use client" at the top of file, then it becomes basically what is the default today.
And i'm gonna say it would even more confusing as if your default is client components, then i suppose you won't need an annotation at the top of the file right ? But when imported from a server component (let's say you mark it with "use server"), you'd need to add the annotation again ?
If you say, we have to be explicit in marking every component as client or server one, but then you loose portability of some components that could be shared by both the client & the server, something like a <Markdown /> component that could be as RSC when you render it in a blog or comment, and act as a client component when used for previewing the content in a editor or CMS for ex.
The default is RSC because the server is the first thing being hit when a user makes a request to your app, so it allows you to do things in advance, prepare some markup and only decide what you want to send to your client.
But with server components as the default, something like this actually can and will happen. Every unmounts and remounts server components, which can be a lot if you put the whole app into a server component (as opposed to just the fetching).
No it doesn't, when you use Layouts in Nextjs, they persist between navigations (with their state). Server components are not "unmounted" and "remounted", they don't run on the client, what happens is react updates the DOM directly, if you consider updating the DOM a big operation here, consider that it is the same with client components, react needs to update the DOM when you change pages, or else you would always see the same page.
I advise you to watch dan abramov talk at remix conf to understand more the mental model around adding interactivity : https://youtu.be/zMf_xeGPn6s
No need to change the defaults, just add
"use client"onItemsthen what you have is that only<Fetch>is a Server component here, and since all the imports from a client component are considered as client components themselves,Itemsand its children become client components.
Right but it's easier to miss a component like <Items> which maybe should be "use client", than to miss a client component that should be a server component (hard to miss). So I think the server default is likely to cause more mistakes. It's potentially a premature "optimization".
The thing here is about loading the bundle necessary to render your different UI versions, by default with client components, it would have to load all the versions on the client, even though they are not used, this is unavoidable as all the client imports are bundled, while importing a bundle into a server component only sends that bundle if it is rendered.
You can use middleware and/or dynamic imports (the latter does indeed have layout shift problem), but fair enough the DX for RSC is better for many combinations of experiments. In this case I would consider making an <Experiment> server component instead of making my whole app start out server-side and then carving out client components.
If you say that client components are the default and using server components means you send them to the root, it becomes just the same design as today. Because you'd have to tell at some point that the imported component is a client one right ? So you'd have to add a "use client" at the top of file, then it becomes basically what is the default today.
If you make client components the default, you can remove the behavior that components inherit their importer's type (since the flip side of "client components can't import server components" is that server components can import client components). In that case even if you made parent components server components, their child components would still be client components and you wouldn't need to add any additional annotation.
No it doesn't, when you use Layouts in Nextjs, they persist between navigations (with their state). Server components are not "unmounted" and "remounted", they don't run on the client
Yeah layout.tsx and page.tsx are good for server components but Next.js app/ also makes child components server-side by default, and those do get loaded and mounted per page. Even page.tsx will get refetched if you revalidate, but this is generally acceptable.
The default is RSC because the server is the first thing being hit when a user makes a request to your app, so it allows you to do things in advance, prepare some markup and only decide what you want to send to your client.
This is a good reason for:
- SSG/SSR to be default
- layout.tsx and maybe page.tsx to be default server components
but I'm not sure it's a good reason for child components to default to server components. Even if they're non-interactive I don't necessarily see that as a good reason, because of the potentially redundant payload.
I would be a lot less skeptical if layout.tsx and page.tsx were default server components but without extending to make children server components by default. In fact, that might actually be better than my original idea of having all components be client default (it's closer to being "smart" about which should be server and which should be client).
If you say, we have to be explicit in marking every component as client or server one, but then you loose portability of some components that could be shared by both the client & the server, something like a component that could be as RSC when you render it in a blog or comment, and act as a client component when used for previewing the content in a editor or CMS for ex.
Fair point, I haven't thought about this. My suggested way (not inheriting parent's directive) would probably require the directives to be refactored.
I advise you to watch dan abramov talk at remix conf to understand more the mental model around adding interactivity : https://youtu.be/zMf_xeGPn6s
All of Dan's stuff on RSCs is very good, I have no issues with it. I just question the way the Next.js meta-framework uses RSCs. It is like a "glass half-full" vs "glass half-empty" thing; should server components be the rule or the exception? All of these things like fetches, rendering libs, etc seem like exceptions to me.
Hi @llllvvuu, just stumbled upon this thread which I find verrrrrry interesting.
I basically work on two types of projects:
- many static websites
- a realtime collaborative web app
A static website is generally updated every month by some editors through a headless CMS. No matter how big is the website, I think in this case RSC would always be the best choice, as payloads can be cached into the Data Cache, the Full Route Cache AND the Router Cache.
The Data Cache would be refreshed only when the website administrators (or editors) make changes in the CMS (it would need the CMS to send requests to the Next.js app in order for it to revalidate associated tags).
I use this technique in all my recent projects and this is very powerful:
- Pages are generated at build time
- Users will see the static pages (cached through Full Page Cache)
- Once the editors make a change in the CMS, it sends a GET request to my custom
/api/revalidateroute with the tags to be revalidated - This Next.js route will
revalidateTag() - All the pages that use fetch with those tags are now considered stale, and the fetch is reexecuted
- During this revalidation time, users will see the stale page
- Once the new data is fetched, the page is generated and cached into the Full Route Cache
- Next users will see this new page
In this peculiar case, I see only pros for RSC, cause no fetch is done on the client side (except the internally made ones, cause the client does talk with the server through some kind of network), so no need to install a library like SWR, or a GraphQL client, or any other tool that would simplify the exchanges between client and server.
RSC are also much more easy to use for a beginner.
But as you can see in this example, as everything is static, we don't leverage at all all the Suspense, loading.tsx and async tools for delayed layout. Because we would never need to.
In my other type of project (which I'm building at the time and this is why I've came across this thread), I have an application that will be used by many users, on a shared group, and they're supposed to use the app everyday and to make changes several times per hour - and their coworkers will need to see those changes when they happen.
Disclaimer: I'm not talking about a realtime app (though I'd like it to be that way, but I'm such a noob with realtime), but an app that is fully dynamic.
In this case, I can't leverage Full Page Cache as rendered pages depend on the user's auth cookie to display personal information.
So every time I visit a page that contains personal information, it will perform dynamic rendering, which means that the client will send a request to the server, which then will fetch data (or cached data) and render the RSC, then send it back to the client.
If I just want to display a list of users (with their names and a picture), the amount of fetched data is a very small JSON, while the generated HTML (which is then compressed into RSC Payload binary format) would be - I think - bigger.
And the difference increases while I use the app constantly: reloading just the JSON list of users would add up only few KB, while reloading full RSC Payload each time I get on the /users page would have a I-dont-know-how-much-bigger weight to the network bandwidth. Especially if I have a very long subtree, with a ton of buttons, of decorative elements, of CSS, and stuff.
Though I have a doubt on how optimized is the RSC payload: If the changing part is just the <p>{user.name}</p> in a RSC, will the server only send that changed part to the client? Or will it send also all the associated sub-components that do not depend on data, but sits next to it?
If only the changed part is sent, then maybe my suppositions are wrong.
But if everything below a RSC (i.e. all server and client components nested into it) is rerendered and sent to the client, then we surely must think more thoroughly about how to architecture our apps.
There is also another aspect that we haven't addressed: the server stress.
If I have hundreds of users on my web app, all requesting personalized pages through their sessions, it would mean my server would have to render each one of them in parallel, which is IMO a vertical scaling issue.
While in the mean time, if my app is rendered client-side, the server stress would only happen a few times, all the subsequent renders tied to data fetching being done separately on each client browser. This is horizontal scaling.
Though I say all that but I have no data on how big a problem it would be, and this is actually my biggest question: what weight should we attribute to each one of these parameters?
Also it worths knowing that in a web app with mutations, you would enjoy instant UI changes: you click on an item in a list, you edit this item's name, you get back to the list and tadaa you see the new name in that list. With client-side this is very easy thanks to tools like SWR, which allows optimistic changes by directly editing the locally cached values while the request is being sent to the server.
With RSC, after editing your item's name you would need to leverage Server Actions - which still are an experimental feature - in order to revalidate tags associated to your list. And when you get back to this list, you'd encounter either stale data, either a loading state. But next.js seems to have a useOptimistic hook in order to directly mutate local data before the server data gets returned (but if we use a hook, then we use a client component, so... meh).
Errrr... this is so complicated -__-
In favor of RSC we also have the security argument: we can hide the API endpoint from the user.
For some people it can be a strong argument.
For me, I don't know. Security is a concern, but as long as this API endpoint is secured, what's the deal?
So if I summarize all this into criteria, I get this:
- Does the client's downloaded payload matters?
- How much stress can the server hold up?
- Do we need fresh data?
- Is the data depending on sessions?
- Is security a concern?
- Do we need optimistic UI?
- Do we need instant navigation?
- Can the data be cached on a session basis (i.e. can I cache the same request for different users with different responses)?
- How big is the data compared to the rendered payload?
- How frequently is the data invalidated?
- How well modeled is the data returned by your API (i.e. do we need a viewmodel in order to simplify the API response)?
- Does the carbon impact of the app matter?
And I surely miss a lot.
What is really hard in that, is that we must give to each one of those criteria a weight in our decision. For instance, for some app, security will have a weight of 2 or 3, while having fresh data would have a weight of 0.5.
Then for each one of those criteria, determine how positive would be the RSC compared to the client components (on a scale from 0 to 9).
This is a great app to simplify this calculation: https://app.ruminate.io
I hope it has furthered the debate (while I have the feeling of being more confused than before... haha)
2 replies
Hi @nagman
This was an interesting read, and I can emphasize with you that it is confusing, but I have some notes (and maybe answers) about the arguments and questions you raised.
If I just want to display a list of users (with their names and a picture), the amount of fetched data is a very small JSON, while the generated HTML (which is then compressed into RSC Payload binary format) would be - I think - bigger.
This is right, usually the RSC payload is usually larger, especially if for a small JSON payload you render a bigger chunk of UI. but a little side note : it is not a binary format but a text format that you can even inspect, I can suggest you to check out rsc-parser.vercel.app which is an app that can help you see what is contained in that payload.
Though I have a doubt on how optimized is the RSC payload: If the changing part is just the
{user.name}
in a RSC, will the server only send that changed part to the client?
No, but it will render all of the server components on the server (else they would not be called that right 😅?) and send their full payload, while for client components it will only send props (in the same payload) and rerender them on the client, so at least you have one bit of optimization here (it may also be one argument for using more client components).
If I have hundreds of users on my web app, all requesting personalized pages through their sessions, it would mean my server would have to render each one of them in parallel, which is IMO a vertical scaling issue.
I don't see why it is a vertical scaling issue there, it is a scaling issue in general, and each page rendering is initiated by a user request, you can apply any solution to scaling here.
If you are on a serverless hosting (ie Vercel), this is not much a problem, AFAIK each request is already processed in parallel and not made to only one server, it won't slow down response times normally, however it may make your bill go higher, but then again I think it would happen anyway if your app got suddenly popular overnight with a bajillion users.
If you are hosting on a fully fledged server (for ex: a VPS), then you have to think about it... Though for what's it's worth a single server of 4gb RAM that you can pay 20$/month can handle quite a large amount of users before being severely slowed down.
While in the mean time, if my app is rendered client-side, the server stress would only happen a few times, all the subsequent renders tied to data fetching being done separately on each client browser. This is horizontal scaling.
The problem is that, you shift the issue elsewhere from the server to the client, if you do heavy rendering, whether it be on the client or the server it will have some performance impact, you just push the problem to individual clients who may not be on the latest high end device.
Usually this is tradeoff and you would see people recommend one or another depending most of the time on the average time of a user session :
-
if your app is supposed to be opened and used for a long time most of the time, then you can prefer rendering on the client, even if you download and render an enormous amount of JS, it is mitigated by the fact that the user won't feel it most of the time they use your app, and you can at least count on caching of static assets (by CDN or the browser) to speed up the boot time after their first request.
-
If your app is supposed to be used for a short period of time and the user can just bounce right after, then rendering fully on the client can be bad, especially if you render an enormous amount of UI that also needs to fetch some data before rendering, if you compare the perceived performance, most of your users will feel your app is slow, cause most of the time they open your app, they are greeted with either a white blank or loading spinners, while all they want is just to check one thing then leave.
-
Then again we have solutions like SSR (without RSC), that renders the fully fledged UI, but download the JS afterwards, but it is the same as SSR with RSC because you still end up prerendering your UI in the server, plus with RSCs you can save a little bit of JS that would end up otherwise on the client, making the UI load a little bit faster because you don't have to download and execute the same amount of JS.
Also, unless you fetch your data from another place other than your server, if your app is very dynamic and needs to fetch data most of the time, you still stress your server, it is lesser than if it was rendered on the server but it is not negligible.
The thing is that, IT DEPENDS, but client components and server components have different advantages to them, you don't need to go fully RSC and avoid CC at all cost, it is futile in my opinion, because you will still need some part of your UI to be interactive. CC are still pre-rendered on the server, so you don't loose SEO when using them with RSC.
My personal experience about how I decide this :
- For fetching data, mostly everything is done in RSCs, and I only fetch data on the client when I can't do it the RSCs, I.e anything that is initiated by the user that don't result in a full page navigation
- For rendering I try to use RSCs as much, and if I need interactivity I put a "use client" on top, if it is very annoying to make a component a RSC, I just put a "use client" on top and i am done with it, I can still come back to it and refactor the composition to use RSC.
I prefer always fetching on the server because that is where I have all the resources available and can ask for cookies, headers, perform a DB query, and even cache a query for one or many users, and if a data-fetch can be slow I just put it in a Suspense boundary so that the user can at least see something while it is loading.
Hope this helped a little
Wow, big thanks, that helps me a lot!
Thanks for the parser, that's interesting too.
Though it's strange that RSC Payloads are in text format while next.js says in the docs that:
The RSC Payload is a compact binary representation of the rendered React Server Components tree.
But I've seen some errors elsewhere in the docs, so it may be a mistake too.
Now with all that information, I think I can safely go with CC because my app is going to be used almost all day long.
Also I don't know which hosting solution I'll choose, but as it will be an app deployed for European users, it will need to follow the GDPR rules, and I don't think Vercel or AWS fulfill them (the mere fact that those are American companies could act as a no-go).
So in doubt, let's make the app the cheapest in resources in order for it to turn on my 2013 computosaur.
And concerning the source of data, it will be handled by an external API - maybe deployed on the same server, I don't know, but separated from the Next.js app. So the stress could be delegated to another server instance.
So... yeah. Let's go for CC! (and of course RSC for some parts)
This is a hard decision to take cause I have this novelty bias deeply fixed in my head, that tells me "but look, RSC are the brand new thing! It certainly must be better!"
Yet RSC are merely a simplified version of a cached fetching library coupled with an API - which is what I'm about to build with CC anyway, but more adapted to its use.