In the olden days, HTML was prepared by the server, and JavaScript was little more than a garnish, considered by some to have a soapy taste.
After a fashion, it was decided that sometimes our HTML is best rendered by JavaScript, running in a user's browser. While some would decry this new-found intimacy, the age of interactivity had begun.
But all was not right in the world. Somewhere along the way, we had slipped. Our pages went uncrawled by Bing, time to first meaningful paint grew faster than npm, and it became clear: something must be done.
And so it was decided that the applications first forged for the browser would also run on the server. We would render our HTML using the same logic on the server and the browser, and reap the advantages of both worlds. In a confusing series of events a name for this approach was agreed upon: Server-side rendering. What could go wrong?
In dark rooms, in hushed tones, we speak of colours. We joke of nulls, commiserate about queues, but of colours we only whisper.
That is because colours can hurt you. We shield our juniors from this knowledge, not because it's the wise thing to do, because once you confront the abyss there's no turning back.
The common colours are no secret. Is a function synchronous or asynchronous? Does it mutate the arguments provided, or meekly return a value? Do I need to worry about it throwing an exception?
In a certain light, colours are merely a way to categorise certain types of functions. Functions of a like colour can be used together without much consideration.
It doesn't take much examination to see that colours beget complexity. Much of the art of application-level architecture is appropriately reducing and grouping colours to oppose this complexity.
In the olden days, we had few colours to worry about. The task of managing incoming connections was abstracted to a simple mapping of request to response.
All functions blocked execution for a while. Some would talk to databases or queues, and we were either content to wait, or to fire and forget while we returned the response.
It's different in the browser. Our code competes with the user's ability to interact, and therefore we must be careful. We embrace asynchronous code, and with it all the complexity that comes from another colour.
Server-side rendering (SSR) poses yet another problem on top of this one. Some functions were not designed to be used in both Node and the browser, and so we find another colour lurking.
In order to support applications that make use of both server coloured functions and client coloured functions, bundlers were built, as clever as they are obtuse.
Say you want to read a file as part of a request. This is only allowed in certain functions, designated by the framework to be server-coloured.
You cannot use a server-coloured function at the top level, since that implies it should be included in your browser, and you cannot use a client-coloured function with the framework's server-coloured functions.
Next.js describes this approach as "smart bundling". This may be smart, but it is not wise.
It's hard enough to ensure that you're only using the correctly coloured function in the right place, until you consider that one of the main advantages of this sort of framework is sharing code across the server and the client.
It's entirely up to you to ensure that you correctly manage your function colours. If you accidentally use the wrong sort of function in your shared code, your application will not work.
If you're lucky, the bundler will catch this and throw a seemingly unrelated error. More likely, you'll experience a cryptic runtime error, followed by days of agony if you don't understand this problem. There is very little available in the way of static analysis to make this easier.
To me, this seems like a very bad idea. What's worse, this important colour is generally treated as a footnote by the creators of these frameworks. Most developers only realise how bad this problem is when it's too late.
I don't think that SSR apps are fundamentally a bad idea. That said, the way we're going about it right now is terrifyingly complex and error-prone.
If you're considering using one of these frameworks, I would recommend you carefully consider if the complexity is worth it, especially for less-experienced members of your team.
For those who do pursue SSR, I advise you to work towards better static analysis to help manage these problems. Rust has built a career by explicitly recognising that lifetimes exist and that we need better tools to work with them. A similar opportunity exists here for SSR apps.
For an alternative perspective on this, I'm the developer of Perseus, a Rust framework that supports SSG, SSR, incremental rendering, etc. From my experience, a lot of the problems of mixing server-side and client-side code come from ambiguities in dependency management and misunderstandings about how state is passed through an application.
Compiled languages like Rust, while much slower in compiling apps, are often faster at runtime (Perseus scores better than Svelte in some Lighthouse metrics, for example, though Wasm still has a way to go on some DOM optimizations), and they're usually much better at delineating dependencies. For example, I can easily declare a 'crate' (Rust dependency) that's only used on the server, and separate that from one used only on the client.
The big misunderstanding I've found from a lot of my framework's users is with regards to fetching data, and the ambiguities of doing one thing on the server-side and a quite different thing on the client-side. I think that can be partially solved by putting server-side code in a different file, but the important thing is to have clear state flows through apps. You generate state in one place, and then it's processed by the framework, and then you can use it in another place.
Fundamentally, SSR should, in my opinion, be about state generation. It's easy enough to optimize apps to prerender some HTML on the server these days, the tricky part comes when you're figuring out what to load and when (you need to load user-specific information in the browser, product-specific information on the server at runtime with caching, layouts at build-time, etc.). To make sense of that for users, I think framework developers (myself 100% included) have to make what the 'black box' framework is doing more transparent. When people think the data they fetch on the server-side is shared magically with the browser, misunderstandings pop out far too easily. If we can make it clear that the data are serialized, stored as JSON, passed over the network in some way, deserialized, processed, interpolated, made reactive, etc., and if we can communicate that in a beginner-friendly way (still working on that one...), then I think we can start to fix this issue.
The reason a lot of frameworks have these kinds of problems as a footnote is I think because the developers don't see them as a problem. When you've built a system for moving state around, and you understand it, things are clear to you. Then, suddenly someone starts asking about why they can't use a server-side function in a different place, and it's obvious to you because you know how state flows through the app, but not to them, because the documentation treats the framework's internals as something you shouldn't have to worry about. I think that's the information we need to get across to users if we want to solve this problem long-term without making significant infrastructural changes to the existing framework landscape (and, if we were to make such changes, I think, as the comments on this post prove, the best approach is far from universally agreed).
As a side note going to @joepuzzo's point, I think making the internals of frameworks more transparent in documentation would go a long way to fostering a more productive environment in framework development. When we can all learn from each other, and figure out why one framework is faster than another, regardless of our skill level in a particular language, we can hopefully bring one framework's advantages to another framework. The open-source community should be about cooperation, not competition.