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.
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.
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.
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!!!
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.
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:
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.
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.