One of the most dynamic areas in software development today is front-end architecture. Several innovators are pushing the state of the art to devise more powerful ways to build dynamic user interfaces. Much of this work is happening at a furious pace and right out in the open.
What is hydration?
Much of the activity around improving modern front-end architecture is focused on what’s called hydration. To understand what hydration is and why it’s central to modern front-end architecture, let’s get a grip on the high-level concepts at play. To deliver the wonder of reactivity, every framework must handle the three aspects illustrated in the diagram below.
The basic message in the diagram is that the framework is responsible for framing the view, holding the state, and managing the interaction between them. If you are familiar with the MVC pattern, you’ll hear that echoed here.
Once these three pieces are in place, you’re good to go. The user can see the page and interact with it.
This approach has the wonderful benefit of simplicity, both for the code at work and for the human minds trying to understand it. It also has a big downside: The initial page render has to wait on everything, and the user has to sit through all of that network and browser churn. Also, unless care is taken, the page will tend to display and then embarrassingly rearrange itself into the final layout. Not a good look.
This inspired developers to try rendering the initial page on the server first (server-side rendering or SSR) and send it over. Then, the user has a decent page to look at while the rest of the code and state is sent and bootstrapped. This is a great simplification but that’s the basic idea.
The time it takes to get the basic layout in place is called first contentful paint (FCP). The next milestone the page needs to reach is measured by time to interactive (TTI), meaning the time until the user is able to actually use the page.
The process of taking the initial page and making it interactive—that is hydration.
Limits of server-side rendering
The bottom line is that SSR tends to improve FCP but worsen TTI. Thus the goal has become striking a balance between the two while maximising them both, while hopefully maintaining a pleasant developer experience (DX).
A variety of approaches have been proposed, adopted, abandoned, modified, and combined in this effort to improve hydration. Once one starts looking at the implementation details, one is amazed at how complex it becomes. A balanced enhancement of FCP and TTI with a decent DX? Sounds easy but it isn’t.
One reason for the complexity is that we’re smack in the middle of sorting through all of the trade-offs; it’s an unfolding scene. Once the way forward crystallises though, we should expect two results from the client architecture that emerges. First, it should create web apps that feel “next generation,” in the same way that well-built apps today provide a subtly but clearly better experience than one from a few years ago.
Second, and perhaps even more importantly, our improved client architecture should have far reaching consequences beyond better performance. By wading into and resolving the complexity, front-end engineers will arrive at a better model, for both the system and the mind. A better architecture actually represents a more powerful heuristic. This results in follow-on benefits that are often unpredictable.
You can see this in action with reactivity itself. Reactivity burst onto the scene because it offered a way to offload state binding from the developer’s brain to the framework. But the benefits didn’t stop there. The architecture became not only simpler, but more consistent. This netted performance and functionality gains across the board.
Approaches to improving hydration
The basic trick to improving the hydration situation is to look at things more granularly. By breaking the view, the interactivity, and the state into smaller pieces, we can load and activate them stepwise, optimised for FCP and TTI. Here is a tour of some of the approaches.
The no-JS approach is seen in SvelteKit, for example. This doesn’t do anything for those pages that require reactive interaction, of course. Frameworks still must address hydration on those pages that act as SPA.
Astro has championed the idea of island architecture. The idea is to determine which parts of the page are static, and which parts require reactivity. With that knowledge, you can fine-tune the loading of the page by ignoring entirely the framing content that never changes, and then loading the other parts (the islands) only as needed.
It’s useful in grokking this idea to note that it is targeted at improving SPA. That is to say, all the static content you identify is able to just sit there, doing its job without any performance hit. All your client-side state and navigation is maintained.
On the plus side, this approach allows you to delay loading each island until something happens to make it necessary (e.g. scrolling into view, a mouse click). On the downside, in practice it often results in loads that occur at a particularly inopportune moment (just as the user is doing something).
Lazy loaded boundaries
Features like React’s Suspense component offer an approach that keeps the basic hydration model in place, but decompose it along boundaries that are then lazy loaded. This has the advantage of keeping much of the familiar process in place, but the downside of requiring a lot of thought and tuning on the developer’s part to achieve good results. Mentally, the developer is in the position of bridging the world of component layout and build-time code splitting.
Furthermore, lazy loading can only help so much, as much of the framework still has to be shipped up front.
Resumability is an idea that was introduced by the Qwik framework. Qwik dives deeper into the elements of the application and creates lazy boundaries across them. In a way, you could view it as a highly sophisticated form of lazy loading bounds. Resumability means that the client can pick up where the server left off, and keep things in sync in a fine-grained way.
React is rolling out the idea of server components and a related performance improvement called streaming. Here is a description of how server components work. In essence, server components allow you to identify which parts of the app can be run entirely on the server, thereby avoiding any client-side render penalty.
Streaming is another evolving React technique related to Suspense. The idea here is to allow for framing content like HTML to start shipping to the client before all required data is even ready on the server. This can then be applied as component interaction occurs.
Partial hydration or progressive hydration
Things get a little muddy with these terms. Astro describes its island architecture as partial hydration. That’s simply to say, only certain elements of the page are hydrated at a time. This is also sometimes called progressive hydration. Both of these terms are sometimes applied to other techniques.
We really have three terms here stepping on each other’s toes: islands, partial, progressive. No matter, the main idea is the same: We need to decompose the structure of the app into smaller chunks in order to make it load more intelligently.
Let’s try to disentangle the terms a bit. Let’s say island architecture refers to Astro-style chunks of independent interactivity within a static frame.
Moving up, you could say the whole idea of decomposing the UI is partial hydration, and Astro’s islands are one example of it. We can’t do that without peril, though, because Astro == island == partial is already floating around out there. Also, partial seems to suggest an incomplete state of hydration, which is misleading.
Then again, progressive invites confusion with progressive web apps (PWA). Maybe partitioned hydration is a good term for the overarching idea.
Front-end architecture evolution
Among these people are Ryan Carniato (Solid) and Misko Hevery (Qwik). Both are pushing the state of the art, releasing code and information to the rest of the world as they go. Two good places to start with Carnatio’s work are here and here, and two for Hevery’s are here and here.