DEV Community

loading...
Cover image for Server Rendering in JavaScript: Optimizing for Size

Server Rendering in JavaScript: Optimizing for Size

Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
Updated on ・7 min read

Continuing from where Server Rendering in JavaScript: Why SSR? left off I want to talk about the different techniques JavaScript Frameworks are using Server Rendering to optimize the performance of their websites and applications. There are numerous techniques and every framework has its own take.

In this article, we will cover all things related to size. The amount of JavaScript you ship to the client can be heavy on the network, and it can be heavy on the CPU when you consider both parsing and execution.

So how are frameworks optimizing for bundle size? Last time we talked about Code splitting. What else is being done?

Encoding View Code

This is the idea that we can compress our Components even further than the executable JavaScript when shipping over the wire.

I am referring to things like Glimmer's ByteCode or Facebook's Prepack. The idea is that if you can codify the instructions into fewer characters, and possibly even pre-solve parts of it the way you would reduce an algebraic equation. If you haven't seen Prepack you should try it out you are in for a bit of a treat.

While the Prepack experiments haven't yet borne fruit, Facebook is back at it again with React having come up with a serialized form of their VDOM representation of their Server Components.

These techniques clearly benefit Virtual DOM libraries where our views are a series of instructions. LinkedIn reported a 50% reduction in component size, but size isn't the only benefit here. JavaScript is about the most expensive things to parse in the browser.

But what about non-VDOM libraries? At first thought, you might think of a compiler like Svelte or Solid. But this is not the same thing. While they reduce the code into real DOM instructions, which allows them to have a much smaller core runtime, this approach can actually increase the code size per component.

However, libraries that use the real DOM have other ways to optimize component code size. One such way is Template Cloning(using DOM Template Element) the static parts that can drastically reduce the number of instructions. In so most of your components can be encoded as strings that already benefit from being Gzipped. As it turns out template cloning is more performant than creating nodes one at a time.

Partial Hydration

Alt Text

When a server-rendered page arrives in the browser and we want to attach the interactive JavaScript to it we call this hydration. It's a lot like the first render of a client rendered application. We traverse the whole application creating components and state, attaching event handlers, but we don't re-create the DOM nodes.

However, do we really need all those components in the browser if we rendered everything on the server? The answer is often no. There are plenty of examples of static parts like headers, footers, navigation. In so you can view the interactive parts of the page as isolated islands. This can reduce code size dramatically.

Effect of Partial Hydration

eBay's Marko Team ran some tests toggling the Partial Hydration off on a few pages of the eBay website.

To understand how this works, I find it easiest to imagine there are 3 types of components. The topmost components like the page itself and header and footer are "Server" components that are completely static and do not need to be sent to the browser. The next set are "Stateful" Components which can be rendered completely on the server but have local state, event handlers, things that cause them to update. Finally we have "Client" components that need to be completely rendered in browser.

However, every framework has its own way of handling these. For most VDOM libraries there is no difference between "Stateful" and "Client" components because they need to build the VDOM tree anyway. For reactive libraries with Template Cloning, there is very little difference between "Server" and "Stateful" components since they can skip shipping the template in either case and only have as much code as is needed to hydrate which for "Server" components is basically none.

Note: Vue being both reactive and a VDOM uses a similar static hoisting method with string encoded views. While it might not be able to leverage being able to hydrate at a sub-component level it can still reduce the majority of code without the complexity of moving application entry points.

To pull this off, at build time analysis or heuristics (perhaps a file naming convention, or config file) are used to ensure the client bundle does not get the unneeded code. Alternatively, it can be manual by creating your own roots. Custom Elements can actually a pretty good tool for this, bringing their interactivity in a sea of native elements client or server(with the right library).

This is an area that frameworks are working on improving. Marko is the only framework today that automatically handles this for the end-user without any manual intervention.

Unfortunately, it isn't always that simple. And I know what we have covered so far is not simple, but there is more. In the example above, eBay is not a single page application. Even though there are interactive portions and places that need to redraw, primary navigation is handled by rendering new pages from the server.

As you have probably realized by now is once you need to render the page in the browser you need to bring all the JavaScript code. Even if you don't need all the JavaScript initially you will need it if you navigate back to that page. They all become "Client" components.

Perhaps the most obvious way to address this is to create multiple bundles. You aggressively partially hydrate the initial page even under the router, and then load full client renderable bundles for any navigation later, including back to the original page. This can deliver on the promise of Partial Hydration and less JavaScript on initial load. But it does mean code duplication. You will eventually be sending (different versions of the) the same Components twice. But after the fact maybe that's ok. Vue has been exploring this approach with VitePress.

React Server Components have an interesting take here. Just continue to render these portions on the server even after the first load. We can load our client router in the browser but then re-render each nested page on the server. So we can regain our Islands below the client router and our pages can return to being mostly static even with a SPA.

Interestingly both of these approaches require special consideration around routing and in React's case a dedicated backend solution.

Analysis

Naturally, the first thing I want to do is put these to the test, but it would be anecdotal at best. The first thing that came to mind was the comparison of Svelte Component Scaling compared to React. Some sort of test to see how much difference a small library that ignored all this compared to a large library that didn't.

Something like byte code might reduce size for a VDOM but is it smaller than GZip compression on a string. Which is more expensive to parse? Is it worth the extra client-side code to handle this? The same goes for topics around server components and partial hydration. At what point does a now larger, 50kb React intersect with a 4kb library?

But these are limited comparisons. If the eBay example earlier is any indicator these numbers can vary greatly. Real large apps have a lot more code than even the component code. It's the 3rd party libraries. No toy demo/benchmark is going to demonstrate this. The biggest win is not just not shipping the component code but not shipping heavy libraries.

That is a pretty good case for React Server Components which can avoid ever shipping certain JavaScript to the client. Marko's multi-page approach also achieves this. Of course, there are other ways to offload work to the server. Also if it doesn't block initial hydration, loading the rest of the JS after can not be terribly detrimental assuming it can be cached afterward. I will look more at performance optimization in the next article Server Rendering in JavaScript: Optimizing Performance.

Conclusion

The thing to remember about size is, with pretty much every technique your mileage will vary based on the nature of pages you have and the scale of the project. There are plenty of applications where these techniques are not worth the effort. Sometimes due to the framework. Sometimes due to a highly dynamic nature so there are minimal gains. Sometimes a different architecture is more beneficial and is simpler.

This is a pretty tricky thing to test/benchmark independently. So it might be best to look at examples holistically. Even tree shaking already makes tools like Bundlephobia limited in their use. There are libraries consistently producing smaller bundles than those half their size.

But know every framework is working on mechanisms to address size. It will be interesting to see how effective they will be as more continue to release their versions over the coming year.

Discussion (5)

Collapse
jon49 profile image
Jon Nyman

Another option would be to use something like HTMX and then use a service worker for offline work. Then you could have your page work with no JS (if you use HTMX in the proper way), then use something like HTMX to make your pages a little smoother, and then go completely offline with a service worker.

Collapse
ryansolid profile image
Ryan Carniato Author

Forgetting about offline mode/caching for a second. Is this similar to Hotwire? You basically load just enough JavaScript (looks to be 9kb gzipped) to be able handle making the requests to have the server send back responses and do partial insertions. But from there, there is no additional component code costs.

The only thing that has always had me question these approaches is how they account for client state. I guess with MPA mentality there isn't really much to worry about, but if the interactivity is sufficient enough do you end up with sort of maintaining two conceptual applications. In any case you are right, I do mention these sort of approaches in the last article but then just breeze right by them here. In some ways React Server Components are similar so it will be interesting to see if that influences adoption for the right sort of applications.

Collapse
jon49 profile image
Jon Nyman

Oh, sorry, I didn't notice this reply. I didn't get an email for it :-).

In the app I'm working on right now, client state is kept in memory and if I refresh the back end it will destroy the state. I think traditionally they would just put it in the cookie, but you would need to be careful not to make the cookie too big.

If you have a lot of state it might make sense to go ahead and write a front end. But many business apps don't really need that. So, the right tool for the job. Unfortunately most people don't really need a full front end app and it actually harms the user experience. But sometimes you do need that full front end experience and it makes the user experience much nicer. So, I guess it just depends what you are building. But most web apps could work fine as an MPA slightly enhanced.

Collapse
peerreynders profile image
peerreynders
  • Server component - completely static and only the render result gets sent to the client - not the renderer.
  • Stateful component - rendered completely on the server but has client side state (like server-side rendered HTML augmented client-side with regular-elements).
  • Client component - needs to be completely rendered within browser.

For reactive libraries with Template Cloning, there is very little difference between "Server" and "Stateful" components.

I like the notion of a three-tier organization but I don't see why "client components" can't use template cloning. If I understand correctly "stateful" components are bound to DOM elements created by the browser in response to parsing server rendered HTML. If so, "client components" wouldn't be that different from "stateful components" if they bind to DOM elements created by the browser in response the content template being cloned (unless I'm misunderstanding the organizing principles).

And it's the "stateful component" implementation options that expose the limitations of the component mentality. Clearly it's convenient and attractive from an authoring perspective to be able to wrap a presentational fragment with some code that needs to manipulate that presentation to render the visible portions of its state into a single unified package. But that's a self-imposed constraint (grounded in OO-thinking) - it's conceivable that a cohesive unit of code/behaviour may need to present parts itself in multiple, separate locations on the DOM-tree that aren't collocated or related via predictable parent-child relationships. That is, "UI components" may need to become a lot more humble - to the point that they are mostly glorified templates - "UI templates" - that the smart (non-DOM) code can be bound to. So the generally accepted component mindset could be more of an obstacle than a blessing moving forward.

but if the interactivity is sufficient enough do you end up with sort of maintaining two conceptual applications.

That isn't an issue if those two conceptual applications are compile-time artefacts of some unified design-time view - duplication is only an impediment to adoption if the duplication has to be maintained manually. I see the current practice of conflating server and client capabilities into the same component at design and run time to achieve SSR as an unwelcome source of non-essential complexity. It seems to make more sense to separate the relevant facets at design time while providing a means to correlate them and then "weave" them together at compile time.

Collapse
ryansolid profile image
Ryan Carniato Author

To your first question I meant that when you hoist the static template parts(for cloning) you can just not ship them if the component is "Server" or "Stateful". In all cases the render code is basically hydration.

So if something is "Server" you don't have anything to hydrate and your component is basically a no-op. The only code it would have had to clone the node can safely not be included. With "Stateful" it is the similar, but you still walk to add the dynamic parts. You don't ship the static parts. With "Client" though you need to send the static parts since it needs to be able to re-create that part of the template.

The fact that the decision not to send the template is relatively easy and without almost any other code modification you achieve the 3 tiers since the hydration code is mostly what you would have generated anyway. I was just pointing out that "Server" components could be bucketed in with stateful since really only difference between the 3 is include template or not which only the latter does.

In reality we can do this at a subcomponent level since it isn't the Component that is "Client" but rather the control flow branch. But I thought it was easier to talk in terms of components. With proper analysis we can choose exactly which sub-template parts need static templates or not.

With a Component architecture like you might find in a VDOM you actually need to not include the "Server" components in the re-render tree. Current Marko uses a trick where we identify all the topmost "Stateful" components and forward the props up to them and basically treat them like their own entries. The thing is once we are stateful we depend on diffs so all "Stateful" components are "Client" components. Which means you lose the ability for "Server" components to exist under "Stateful" components. In the more granular approach you could have a lot more static stuff under "Stateful" components as long as it doesn't fall under a "Client" control flow branch which is why we are much more excited about this newer approach.


On the comment on interactivity. To be fair there might be something in HTMX that I wasn't seeing. I was just thinking about like Hotwire and the like. Basically a server template that doesn't take the potential for persistent client state in mind will need to go somewhere else.

React Server Components seem to be a way of making this separation in the same tree. But I think a lot of the angle being taken with a lot of JS libraries is to simplify things by abstracting out the server vs client bits behind common interface. Like if you are loading data from the API vs the Database that piece can be hidden behind your useResource hook. I guess there is some complication there but I'm very interested in this follow the data sort of thing where the component doesn't care where it's rendered. That might be idealistic but I'm thinking I'm liking the potential to write our components once at design time and have the compiler split it out. You only can do that if you write your components the way you would in the client as pure templates are insufficient. Of course Marko's newer approach is that these are same thing so I mean there is that.