So how are frameworks optimizing for bundle size? Last time we talked about Code splitting. What else is being done?
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.
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.
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.
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).
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.
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.
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.
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.