Blog Hero Background

React Architecture Tradeoffs: SPA, SSR, or RSC

A contrast between React SPAs and frameworks that are a hybrid between SPAs and SSR/RSC. The React team recommends frameworks.


User Avatar
Brad WestfallSay hi on Twitter
March 31, 2025
ReactSPASSRRSC

See Our Public Workshops:

Doing SPAs with React coincides with setting up React and a router yourself and following your own conventions. In this way, React is more like a library because it's just one of the parts that you have to assemble yourself. Modern React SPAs typically use Vite because you'll need a bundler and compiler with React, to which Vite is a scaffolding for both.

What about CRA? The React team no longer maintains CRA and doesn't recommend it anymore. They recommend Vite if you're going the SPA route.

If you're just getting into the React ecosystem, React devs associate "React SPA" with "more like a library, without a framework". When React is used with a framework, that's usually considered "not an SPA". There are some exceptions but this is the normal sentiment that framework means "not SPA".

By the way, modern React Router Framework (Formerly Remix) also uses Vite as its underpinnings for build and compile and then adds its own framework stuff as a plugin to Vite.

Understanding SPAs

I often explain how SPAs work at the beginning of workshops because the definition can be confusing to some. In another post I explain SPAs by splitting up the definition into "What SPAs are trying to do" and their "implementation details". Be sure to understand SPAs for the rest of this post.

SPA Tradeoffs

SPAs were the defacto standard way to build React apps for many years. They're great for beginners and are often the first experience React devs get when their job does React.

The React docs have a Build a React App from Scratch page which encourages you to use Vite if you're going to do an SPA. However, in that same page they recommend not doing an SPA and doing a framework instead if you can.

SPAs might seem like the best choice when your backend is not JavaScript/Node since you can decouple your front-end SPA stack from the backend stack. Later in this post we'll cover how you can use a React framework that does Node on the backend while also keeping your Java/C#/PHP/Ruby backend as well.

SPAs are fast once the bundle is loaded, assuming your data-fetching server is fast. They give you a ton of freedom in terms of architecture, and as a static build they're easily hosted as static files.

Here are some considerations for not using an SPA:

Performance: They can be fast for page transitions, but they take at least 3 serial requests on the first page load. They also have bundle size issues and lazy-loading issues.

SEO: Since they take 3 serial requests and the meaningful data is the last thing to load via client-side JavaScript, your SEO quality can suffer a bit.

Data-Fetching Issues: Because of the lazy-loading they suffer from data-fetching issues

Architecture: You may have seen React developers debate things like global state and data-fetching strategies for SPAs. These all end up leading to complex architecture that only needs to exist because of how SPAs work. Doing a framework will help eliminate these types of complex architecture.

SSR and React Frameworks

Server Rendering is not just for SEO

The quote is directly from the React Docs and it's a common misconception about the importance of frameworks that have SSR. For me the big win isn't SEO (although that's nice), it's having a lack of complex architecture in the client.

If you need a better understanding of SSR, CSR, Hydration, and web vital performance metrics, see this post from the Chrome Team.

Frameworks like Next and React Router Framework do SSR and CSR. Any page you visit will serve fully formed HTML (SSR). However, when you go from one page to the next, the next page is a client-side navigation without a refresh (CSR like SPAs). The framework takes care of the lazy loading and code-splitting for you in a way that doesn't have the same pitfalls as the home-grown approach to SPAs. This is why I call React frameworks a hybrid: best of SPA concepts and best of SSR rendering.

Overall, I'd say these frameworks take care of your app in ways that you'd normally have to manually do yourself. That's their primary benefit.

How do they take care of lazy-loading for you? Lazy loading happens automatically because you follow certain conventions that tell the framework how your site is built. They know what to load when you need it. You can even do pre-fetching which means we start to load the next pages before the user even goes there -- creating a very fast experience.

How do they do client-side navigations? The first page visit will get HTML from the server. Then the client loads a small JS file which has some framework code and just enough JS for the components on the current page. It also knows all the next pages we can go to. When the user clicks to go to another page, the framework handles getting more JS from the server while also data-fetching the data for that page in parallel. This all happens without a page refresh, so we're getting the best aspects of SPA and MPA at the same time.

What are loaders? A loader in RR Framework (called getServerSideProps in Next) lets you fetch data on the server when the original HTML file is built. But they also double-serve as RESTful endpoints to do client-side data-fetching when we transition from page to page. Since this moves data-fetching to the framework instead of the component, we can avoid problems with data-fetching with SPAs.

How do frameworks eliminate architecture?

Besides the small things listed above? The short answer, revalidation.

When you want to perform a mutation to your backend (POST/DELETE/UPDATE), the framework has something similar to a loader but its called an "action" in the case of RR Framework. After the mutation is successful, the framework calls its loaders to send new data to the client. The client automatically updates without the glue-work required of an SPA.

Imagine a basic shopping cart situation -- a list of products with "Add to Cart" buttons and the top of the website says "0 Items". The place where you list the products and the main layout where you show "0 Items" are far apart in the React tree and might require architecture glue-work to synchronize data. You might need something like Redux, Context, or React Query if you were doing an SPA.

The user clicks an "add" button and starts the mutation. After the action finishes, RR Framework will revalidate by calling the remote loader that builds the main layout. The message will be updated to be "1 Item" at the top of the site.

I'm trying to come up with a very basic example here, but even for more complex situations you will find that you just don't need all the complex architecture that we were accustomed to in SPAs

How can I keep my Java/C#/PHP/Ruby backend while using a framework?

Maybe the framework idea sounds nice so far, but how can we do this without Node on the backend? With the React frameworks you're required to use Node because that's how the client-side code connects with the server-code for things like revalidation. However, you can connect Node to your other backend in the loader. Think of the loader as being the part that handle's the client request and then routes it to your Java server:

// Example from React Router Framework v7
// Runs only on the server
export async function loader({ params }) {
const user = fetchUser(`java-server.com/api/user/${params.userId}`)
return user // is sent to the component below via props (all on the server)
}
// This is the "page" that will run on the server AND then run on the client
export default function UserProfile({ user }) {
return <Profile user={user} />
}

Where do React Server Components fit into this?

React Server Components are a form of SSR. They're a specification that a framework must implement. The specification allows for a component to run only on the server but they can do data-fetching within. As such, these components do not "hydrate" to the client and can't have state or events. The main benefit of RSC is that less will rehydrate in the client and bundle sizes will be smaller.

I'll be honest, if this is the main idea, I'm okay with how RR Framework already creates a smaller bundle size so for me RSC would just be about less hydration (which is kind of a small win).

There's a lot we could get into with RSCs. We could go on about how they work with client components and how the React team sees you using them with mutations and forms, but this would all lead to a very big post. If you want to understand them more technically, here's two great posts from one dev who likes them and from one who's a sceptic. I've read both and I have to agree with some of the criticisms. They do make things complex with mutations in a way that overwhelms the small benefit of "less hydration".

These might be two years old but they're still very good:

As of right now, RR Framework is finalizing it's support for RSCs but doesn't have it yet. NextJS has support for RSC and were early adopters of the original "vision" of RSCs. Some don't like the original vision for how they should be implemented and are looking forward to RR Framework's and TabStack Start's implementation.

How does the React community feel overall? The survey is in from 2024 and the results aren't great for RSC and how they work with Next. Almost all of these "pain points" of newer React features are related to RSCs and their adjacent technologies for mutations and form actions.

I would say frameworks are great for getting away from the shortcomings of SPAs, but I prefer the RR Framework approach with loaders, actions, and revalidation. My explanation above with frameworks was mostly RR Framework and how Next "pages" architecture works. Next's new "app" architecture is the one with RSCs.

It's worth noting that with Next you can use "pages" and "app" architecture in tandem, so you can still use Next without RSCs.

I'm certainly not a huge fan yet of RSCs (I'll wait to see other implementations), but I don't like to see false information either. There's been this false narrative that "RSCs are the new default" and that "RSCs are the way the React teams wants you to do React now".

The truth is, they're an additional choice for doing React. They're meant to work in tandem with SSR components (sometimes called "client components"), not to replace them.


Photo by Conor Sexton on Unsplash

Subscribe for updates on public workshops and blog posts

Don't worry, we don't send emails too often.

i love react
© 2025 ReactTraining.com