Modern websites are in many cases not really websites anymore, but in fact full blown apps with desktop-grade feature sets and user experiences that happen to run in a browser as opposed to standalone apps. While the much-loved Spacejam Website was a pretty standard page in terms of interactivity and design only about 2 decades ago
we can now go to Google Maps, zoom and rotate the earth in 3D space, measure distances between arbitrary points and have a look at our neighbor's backyard:
What all this leads to is that for many of these highly-interactive, feature-rich and shiny apps that are being built today, the first impression that users get is often this:
This is the time when the browser can first paint any meaningful content on the screen. While the time to first paint metric simply measures the first time anything is painted (which would be when the loading spinner is painted in the above example), for an SPA the time to first meaningful paint only occurs once the app has started and the actual UI is painted on the screen.
This is the time when the app is first usable and able to react to user inputs. In the above example of the SPA, time to interactive and time to first meaningful paint happen at the same time which is when the app has fully started up, has painted the UI on screen and is waiting for user input.
Although this does not improve the app's TTFMP or TTI, at least it gives the user a first visual impression of what the app will look like once it has started up. Of course the app shell can be cached in the browser using a service worker so that for subsequent visits it can be served from that instantly.
The only really effective solution though for solving the problem of the meaningless initial UI - be it an empty page, a loading indicator or an app shell - is to leverage server-side rendering and respond with the full UI or something that's close to it for the initial request.
Of course it wouldn't be advisable to go back to classic server-side rendered websites completely, dropping all of the benefits that Single Page Apps come with (instance page transitions once the app has started, rich user interfaces that would be almost impossible to build with server side rendering, etc.) A better approach is to run the same single page app that is shipped to the browser on the server side as well as follows:
GETrequests for all routes the single page app supports
<script>tags so that the browser would load and execute these scripts and start up the app in the browser as usual
Combining SPAs with classic SSR, we get the best of both worlds - a fast TTFMP plus the benefits of an SPA like immediate page transitions, vivid UX etc. On top of that, patterns of PWAs can be added, for example caching the initial pre-rendered response in a service worker so that it can be shown immediately on subsequent visits or showing the app shell from the service worker cache and then injecting the SSR response into that which is likely available before the app has started up.
When leveraging SSR for SPAs, it is important to get some aspects of the deployment right. Pre-rendering the application for the first request is only an improvement over the classic way of serving an SPA and should not be a requirement to use the app. Neither should it slow down delivery of the app. To make sure these requirements are met, it is important to make sure of two things:
We implemented the patterns described in this post in Breethe, a PWA for accessing air quality data for locations around the world that we built as a tech showcase. Breethe is completely open source and available on github and we encourage everyone interested in the topic to check it out for reference.
Server-side-rendering an SPA comes with a drawback which is added latency. When serving an SPA as a static HTML file that contains only a loading state or an app shell, the HTML file can be served from a CDN which reduces latency. When serving the response for the initial request from a Node server, there will always be some additional latency. The Node server will likely not be running on an edge note in a CDN, thus connecting to it will take the user slightly longer than connecting to an edge node. Also, the server takes some time to run the app, capture what it renders and respond with that, adding further latency. Thus, the browser will know slightly later which scripts to load and thus start loading them slightly later which leads to a longer TTI.
However, with average TTI measurements in the range of several seconds for many sites in particular when requested from mobile devices, an added latency of a few hundred milliseconds might be well worth it in many cases. Without SSR, TTFMP is generally equal to TTI for SPAs and PWAs - with SSR, TTFMP occurs as soon as the initial response is received while TTI is only slightly delayed. So while the user needs to wait slightly longer for the app to be fully started up, the app's UI (and content) is available pretty much immediately. Whether that is a valuable improvement is a case-by-case decision of course. In the example of Breethe, when looking at the result page for a particular location, it's a pretty obvious decision:
While the user might need to wait for a few seconds (e.g. on a low performance mobile device with a spotty network connection) for the app to have fully started up and be interactive, the air quality data that they are interested in is available immediately.
I will elaborate on how exactly the approach works in the next post of this series so stay tuned!