Maybe you don’t need that SPA

There’s an article by Tom MacWright that’s been gaining some attention in the twitter-verse: Second-guessing the modern web. I’d recommend reading it if you haven’t already, but I’ll give a brief summary so you have some context for the remainder of this article:

It seems like the norm for web development today is a single-page React application. This makes it easy to add interactivity to your pages, but it comes at a pretty big cost. Techniques like bundle splitting and server-side rendering can somewhat alleviate this cost, but they come with their own caveats. Maybe everyone’s wrong.

Dan Abramov agreed that the current status-quo is not sustainable and that long-term the React team thinks they’re moving towards a future where the server plays a more significant role.

Dan goes on to envision a future where most of the code for your components is never shipped to the client, but you don’t have to code the server and the client separately. “The future is hybrid,” he says.

That future has been here: it’s called Marko.

Marko is a UI library which is set apart from the pack in its focus on server-rendered applications. It isn’t some new, experimental thing either. Marko’s been used at eBay for the past 6+ years and is a project under the OpenJSF along with other projects you probably know and use like Node.js, webpack, Express, and ESLint.

Marko’s focus on server-side rendering has caused some to write it off as just another templating language for Node.js. I mean, Marko doesn’t have an official client-side router. It doesn’t even have a community-maintained router. But that’s because Marko is focused on what I believe to be a better way of building web applications:

Marko allows you to build pages by composing components and some of these components can be stateful. Only those components that have state, or other logic¹ targeted at the browser are actually sent to the browser and Marko automatically handles serializing any data from the server needed by these components and mounting them in the browser.

This means for most apps, you end up sending down much less code² than you would for an equivalent SPA — even with code-splitting. And if no components need to be hydrated? Nothing is hydrated.

When a Marko template is compiled, it’s able to determine which components are server-only and which need to be sent to the browser using a pretty simple heuristic: is there a class associated with the component? This information is used by bundler plugins and the server rendering logic.

When one of Marko’s bundler plugins is preparing client-side bundles, it begins walking through your component tree. As it walks, it discovers assets needed by the browser such as images and css. Once it hits a stateful component, it includes the component in the bundle plus a function call that will put the component in Marko’s hydration registry. These assets then get included in the html rendered by the page.

Then the page is rendered on the server. When one of these top-level components is rendered, Marko marks the start and end of the component in the HTML, and associates the data that was passed to the component with this section and the component in the hydration registry. It then efficiently serializes init code with each flush from the server.

The initialization is set up so that the JS bundle can be loaded async. If the bundle is still loading, component initialization is deferred until the bundle is available. However if the bundle is loaded, components are initialized as soon as possible, even if other components haven’t yet been rendered because they’re waiting on data to be fetched.

Image taken from Paul Lewis’s 2016 Article³

Component-level hydration is already here and it’s far better than the status quo, but there’s still room to improve:

You can determine the top level stateful components in your application tree, but even within those stateful components there is often additional static content, including other static components (and by “static” I don’t necessarily mean truly static, but that it won’t update in the browser).

It turns out components boundaries might not be the ideal demarcation of what is server-only vs. what is needed in the client. Just because part of a component can update doesn’t mean we need the whole thing. And we probably don’t need all the data passed to the component either.

How do we get here? Our modern frameworks (Marko included) are centered on their components. And the Virtual DOM paradigm doesn’t lend itself well to hydrating part of a component.

For Marko, we’re building the reactivity into the language itself. We see Marko as a re-imagination of HTML for building dynamic & reactive user interfaces. The current class-based api isn’t going anywhere, but it will exist as a layer on top of the new reactive primitives. In a future version of Marko, it’ll probably be a package that you install separately. If this sounds like “Hooks in HTML,” you’re not too far off.

These new reactive primitives will replace the VDOM in our runtime and allow us to do fine-grained updates and hydration without relying on component boundaries for optimizations. We knew the VDOM wasn’t forever, we even mention it in our docs:

Internally, Marko uses virtual DOM diffing/patching to update the view, but that’s an implementation detail that could change at any time.

I mean, Rich Harris and Ryan Carniato did tell us that Virtual DOM and Components were pure overhead⁴.

We’re excited with where things are going and will have more to share on this front soon!

Learn More

Want to take Marko for a spin? Read the docs and get started with @marko/serve, a zero-config server for .marko files.

Or use npx @marko/create to get a starter-app to play with.

On June 24th 2020, Dylan Piercey and I will be speaking at OpenJS World (a free, virtual conference) on the topic of hydration which is highly relevant to this discussion. I’d encourage you to attend.

A couple of years ago I published an article talking about how Marko’s performance on the server is in a different class than your typical modern UI framework (keep in mind that this is a two year old benchmark, React is now about 2x faster than it was, but there’s still a significant gap).

Marko — if I’m not mistaken — is the only UI library that has true support for streaming from the server. Marko will flush out HTML content as soon as it it ready, even while other content is waiting for data to be fetched so it can render. Not even most traditional templating languages support this.

While this is an old article (from 2014!) and we now use <await> instead of <async-fragment>, it’s worth a read:

Footnotes

[1] Marko components can also be “split components”. These are components that define client-side logic that doesn’t cause the component to update via state. In order to achieve performance goals it can sometimes be necessary to bypass the declarative updates, but we’re hopeful that our future plans will make this kind of intervention obsolete.

[2] Exactly how much less will depend on your app and how much static content there is, but to get some rough, real-world numbers I forced a few pages from ebay.com to do a full page hydrate. This is the impact on the main bundle size:

[3] Marko’s approach isn’t quite what Paul was talking about in his article, but when you reduce the bundle size, the number of components being hydrated, and break up that hydration based on content that’s been flushed from the server it starts to look a lot like his desired outcome. It’s not perfect, but there’s more we’re doing on this front as well.

[4] In both cases, this is hyperbole. There’s going to be overhead to whatever strategy you use to make updates to the DOM and contain those re-computations so you’re not re-rendering the entire app when something changes. Interestingly, on the server though, both are pretty much overhead: your end goal is an immutable HTML string.