Reactive JavaScript: The evolution of front-end architecture

Improving the client-side web experience means overcoming the challenges of ‘hydration,’ a fascinating engineering problem being tackled in many different ways. Let’s dive in.

Reactive JavaScript: The evolution of front-end architecture
drmakete lab (CC0)

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.

Thanks to a number of open source JavaScript projects, such as SvelteKit, Solid, React, Qwik, and Astro, we have a front row seat to the evolution of the future of the web. Here’s a guide to understanding the action.

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.

javascript reactivity IDG

The high level aspects of reactivity.

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. 

The naive, or default, approach is to simply take everything the client needs—the frame, the reactive code, and the state—and send it over. The client (the browser) then does the work of displaying the frame (aka, painting the UI), interpreting the JavaScript, and tying in the state.

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 maximizing 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 crystallizes 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.

Because modern JavaScript frameworks incorporate both server and client, the outcomes of these developments may have broad consequences for application architecture in general.

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, optimized for FCP and TTI. Here is a tour of some of the approaches.

Avoiding JavaScript entirely

One approach that has been absorbed in best practice is to analyze sites for those pages that don’t require JavaScript at all. This relates to the newer notion of multipage apps (MPA). It is a kind of middle ground between single page apps (SPA) and straight-up per-page navigation (default web behavior). The idea here is to find the parts of the app that can be shipped immediately as HTML plus assets, resulting in the best possible SEO and load times. 

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.

Island architecture

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

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.

Server components

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

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.

Partitioned hydration?

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

The activity around JavaScript’s front-end architecture has created some of the most interesting code work I’ve ever witnessed. It’s a space full of passionate individuals who are exploring new conceptual territory and doing the groundbreaking programming to go with it. And they’re interacting and sharing their ideas in an open and collaborative way. It’s a pleasure to watch.

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

Copyright © 2022 IDG Communications, Inc.