Building Webapps Without Reinventing The Browser

We're writing more code now than ever, it seems ironic that to add performance we also add code. Shouldn't it be the other way around? 5 July 2021

# The MVC pattern

In any application, data needs to be collected and displayed to the user for further action. This pattern is so common, there is a name for it: the MVC pattern, or Model, View, and Controller — the model is built and passed to the view by the controller. The order in which the model and view are executed can have a significant impact on real and perceived performance. Further, where the execution happens can also have a significant impact on system-level performance; the further upstream it happens, the more likely that it can be shared downstream, and thus the more likely overall performance is improved.

In a web application, we additionally have to consider application load time, which depends on the size of the Javascript payload. Byte-for-byte, Javascript is the most expensive resource a web browser can use. It has to download, parse, and evaluate the Javascript which can take a significant amount of processing. Further, once the Javascript is loaded to memory, it can be arbitrarily expensive to execute. For this reason, Javascript payloads should be kept small, and lazily loaded where possible.

# The traditional SPA

In the traditional Single Page Application, a blank HTML is served alongside a large Javascript bundle. Before it can be useful, the browser has to parse and evaluate a lot of javascript. Then once loaded, data is subsequently fetched, usually in JSON or more recently GraphQL formats. The server (or CDN) may cache the data to improve the data fetching performance. But the actual view render is repeated for every browser — it cannot be shared across browsers, even if the render function was pure. The user is frustrated with a blank screen, whilst all of this is happening. Similarly, the next user when presented with the same inputs, has to recompute everything the first user did, wasting computations.

# Server Rendered SPA

The logical evolution of this is to perform a Server Rendered view, and hydrate the SPA in the browser. This way, the page is visible much earlier, ahead of the Javascript parse and evaluate phase, and the subsequent data fetch can be skipped. This gives SEO benefits, and the server rendered HTML can be cached, thus shared with multiple clients. The parse, evaluate, and hydrate phase can still be very compute intensive, and delay interactivity. But at least the browser can download and start using associated assets like CSS and images whilst waiting on the Javascript.

# Slow and Fast Data

But not all data cost the same; some take longer to resolve than others. To improve perceived performance, fast data can be server rendered in the initial payload, and slow data can be browser rendered (or server rendered with Hotwire ) later. The browser can then download and start using associated assets whilst waiting on the slow data. This way, a portion of the page is visible and usable much earlier. This technique is similarly used when inlining critical CSS and delaying the rest of the CSS until later.

Where multiple slow data responses are awaited on, each requires its own XHR request. Alternatively, multiple responses can be multiplexed into a single Server Sent Events or Websockets connection.

As if a traditional SPA wasn't complex enough, there's server rendered complexity, and even further sync-vs-async data complexity… We are drowning in COMPLEXITY!!!

# Progressive rendering

It turns out that we already have a powerful framework to do all of this parallelism — the browser. Browsers are capable of progressively rendering partial HTML documents as they are streamed from the server (using Transfer encoding: chunked). With only just the HTML head, or with a few initial lines of the body, the browser can preload, prefetch, and preconnect  associated assets in parallel. It can even render these partial HTML fragments without corresponding closing tags. Even without CSS, it can download responsive images. Using script async, it can download, parse, and evaluate Javascript before the HTML is fully downloaded.

We've been trying to reinvent the browser with Javascript! What we really should've been focussing on, is how to build and stream the HTML on the server to the browser in such a way to make us of its native capabilities.

# Async Server Rendering

Recall the MVC? Most frontend libraries/frameworks provide synchronous view engines. That is, the model needs to be first completely built, and the view thereafter. Now, imagine one that allows each component (and sub-components) to be rendered asynchronously. The model and the view can be interleaved, opening up a whole new class of capabilities:

  • Each component may begin asynchronously rendering after its data promise is resolved. This non-blocking behaviour allows other components to render before it as necessary.
  • When a component has completed rendering on the server, it may be flushed early to the client, activating the browser's parallel engine. Time-To-First-Byte (TTFB) is basically the speed of the internet!
  • With pure components, the rendered result may be stored in a cache, and subsequently reused with techniques like Russian Doll caching  speeding up the render process for all clients.
  • The browser barely needs any Javascript to deliver a fast user experience. Only the Javascript for the current page is needed, no need for client-side routers and state management. Where the page needs no Javascript, none is delivered.
  • The pages are SEO friendly, and it is fully composed. No need for additionally Javascript to fill in any blanks.
  • Subsequent page navigations are new page loads. With code-splitting, any common Javascript need not be re-downloaded, though it needs to be re-parsed and re-evaluated. This is likely very small, and executes in parallel to the HTML download anyway.

Ultimately, the HTML is sent serially, so this technique is best matched to pages with fast data at the top of the page, above the fold, and slow data further down, below the fold. Where this isn't so, the content may be reordered using CSS or Javascript.

There are challenges, of course. Any header or content already sent to the client, cannot be unsent. This can result in responses that are not self-consistent, and may need to be solved with appropriate messaging. Further, the push model could mean that unsolicited content could be sent.

# Where can I get some?

Competition is really starting to heat up in this space now. MarkoJS  has been in this space for years. It compiles its own language to produce output that is very light on Javascript and leans very heavily on the browser's native capabilities. Marko is capable of all the above.

For Hotwire fans, Turbo Frames  can do this too for HTML-first implementations, with very little Javascript in the browser, and your choice of templating and caching on the server.

Javascript based solutions like SolidJS  provide a programming model that should be familiar to most frontend developers today. Even React  itself is entering this space soon too with the Suspense API. These aren't as lightweight though, requiring Javascript runtimes in the browser.

There may/will be others.