Mar 8, 2022

Marking it up ???? — Overview of web app rendering techniques

I’ve encountered many people being oblivious to the way web application rendering works. Initial rendering is an important topic, especially in the context of SEO and performance, so brew yourself a nice cup of tea and pay attention.

Now, I will be talking about customer-facing web apps. When building internal tools or toy apps, it does not really matter. Just pick whichever is the simplest to implement (which is CSR in most cases).

There are three stages of the initial app render:

  1. Initial markup is what user sees while scripts load and start up
  2. Loading state refers to the state when actual data is being fetched after the app has bootstrapped
  3. Loaded content is when all data is loaded and the app is ready to use

Each approach solves the individual steps and their transitions slightly differently, with different nuances and trade-offs. That said, you want to use an abstraction over any of these approaches anyway — this article aims to give you an understanding of what is happening under the hood of those frameworks. Let’s get to it!

Client-side rendering

Aka CSR.

The client is served a plain HTML file that only contains meta tags, script and style links. The element where the app is rendered is empty, as such:

The initial page is usually blank, maybe with customised background colour. The initial rendering flow looks like this:

As the initial markup, the user sees nothing. After the initial mount, they see a loading state of the application while data is being fetched. Only after that do they see the actual loaded content.

The summarised flow:

Since the rendering is only happening on the client, you can get away with all kinds of wild and disgusting things, like accessing window all over the place, global state, side effects affecting the rendered markup, etc.

Just remember — just because you can, does not mean you should. In fact, you really should not. ???? Follow best practices at all times. With everything. Stop feeling sorry for your lazy ass.

Pros:

  • Very easy to implement
  • No need for a server
  • Quick and simple build stage

Cons:

  • User sees nothing for a long time
  • Two loading stages — an empty page and a loading skeleton
  • Terrible SEO ????
  • No need to follow best rendering practices

Server-side rendering

Aka SSR.

The server renders the HTML for the initial app render together with populating all the SEO goodies. There are two types of SSR:

  • render the plain page skeleton
  • fetch data first, then render the whole app with content

There is one rule for all SSR that will make your life a lot easier. Bear with me. ???? That rule is — your server rendering function is a function of the URL, maybe locale, maybe theme, and nothing else!

The main reason for this is avoiding locking yourself into needing the server. If you need the server to do a whole bunch of stuff, it will become a humongous pain in the ass to maintain it, and you lock yourself out of static rendering.

Remember, all you really need for rendering your markup is:

  • the URL
  • locale, if you have different locales
  • theme, if you have themes

If possible, defer all other rendering conditions, like the choice of currency or geo stuff, to the client. It is a minimal UX compromise for a drastic reduction in complexity! ????

Plain

This one I like to call a waste of a good server. All the server does is rendering a loader skeleton for every possible URL. The initial render looks like this:

The user immediately sees a loader skeleton of the webpage as the initial markup. The initial mount is identical with the provided HTML, so the user seamlessly transitions into the loading state. User is displayed loaded content when it finishes fetching.

The reason why this approach is a waste is that the server plays no role at all. It literally just re-renders the same thing all over again. ???? Might as well pre-render everything and serve static files.

Pros:

  • User immediately sees something
  • Seamless initial to loading state transition

Cons:

  • Wasteful server work
  • User still waits for content
  • Mediocre SEO

With content

This is the holy grail of SEO and performance. It does not get better than this UX-wise! ????

SSR done with data fetching is the Lamborghini Urus of web app renderings. It has it all, but costs a fucking fortune, both initially and maintenance-wise. ????

The user is served with initial markup already hydrated with content, so no loading state is present at all. The app transitions seamlessly into the loaded content state after its bootstrap:

Server-to-server communication is much faster than client-to-server, so the initial data is fetched very quickly. In addition, the lack of loading state on the client makes the app ready to use super fast:

“C’mon, there has to be some caveat to this approach! ????” You might say. And you’re right. A slow backend could delay TTFB, which would delay the initial render. In addition, you need a runtime, and a damn complex at that. On top of that, getting data serialisation and hydration working is quite tedious and prone to errors.

Here is a typical flow for a single request:

  1. Collect necessary data requirements of the request
  2. Fetch the data from the server
  3. Render markup with the fetched data
  4. Serialise data and embed them into the HTML
  5. On the client, deserialise data and supply them to the initial render
  6. Bulletproof the initial render to be 100% consistent with the server render
  7. Use the data from the server and avoid double-fetching ????

Luckily, in this day and age, there are plethora of existing tools that handle SSR and the whole data fetching and hydration orchestra for you if you choose to go this path, like Next or SvelteKit. Just pick a tool you like, learn its ins-and-outs, and roll with it.

Pros:

  • Godlike UX
  • Top performance
  • Perfect SEO
  • Server-side data fetching

Cons:

  • Complexity ????
  • Gotta have a server or a cloud function

Static rendering

CSR is the thing you have. SSR is the thing you want. Static rendering is the thing you need.

Let me spill the beans real quick — there are only two types of viable rendering options:

  1. SSR with data fetching
  2. Static rendering, with or without content

In the vast majority of cases, static rendering is the answer. Be honest with yourself — do you really need a Lamborghini Urus? Or is an X5 completely sufficient? ????

While some people really do need the Urus going on, for lobbing purposes for example. But that’s a tiny fraction of the population. Same goes for SSR with data fetching!

Static rendering is simple, cheap, robust and overall offers the best price-to-performance ratio of all the other options.

So what is static rendering anyway? ????

After your app is compiled, you run your pre-rendering script, which renders a HTML page for every static URL your app has. These pre-rendered pages are then served to your users as static files, together with an optional 200.html fallback page, or a 404.html page, in case the requested URL is not pre-rendered.

Plain

For every URL your app has, a loader skeleton is statically rendered. The UX is basically identical to the one of plain SSR:

The user receives a loader skeleton as the initial markup. This seamlessly transitions into the loading state of the app. User is displayed loaded content when it finishes fetching.

If your content is private for users, or you have highly dynamic content and favour simplicity over optimisation, this is the approach for you. ????

Pros:

  • Simple to implement and maintain
  • User immediately sees something
  • Seamless initial to loading state transition

Cons:

  • User still waits for content
  • Mediocre SEO

With content

If possible, doing static rendering with content is the absolute best option out of them all, period. The only requirement is that your content must be rather static, meaning it changes once every couple of days maximum. Think blog posts, training plans, marina listings, or the like.

In most cases, the UX is the same as with SSR with content:

The initial data is readily available, but can be stale, so is re-fetched on the client to ensure freshness. The app is already usable while the data is refreshing, so the whole experience is super fast! ????

The build process is quite more involved than plain static rendering. The pre-rendering build stage involves four kinds of renderings:

  • fallback pages like 200.html or 404.html
  • static URLs with content, like /about
  • loading skeleton for dynamic fallback URLs, like /program/:id, in case :id is not pre-rendered, or a fallback SSR endpoint
  • dynamic URLs with content, like /program/1337, with the full list fetched from the server

The routing also needs a bunch of rewrite rules so that a missing /program/:id gets served the proper loading skeleton and not a 200.html page.

Another great thing about this approach is the possibility to upgrade to a full SSR if the need arises with ease. Since you already have a function for rendering markup hydrated with content, you just hook it up to a server instead of pre-rendering stuff statically.

Again, there are lots of existing solutions to handle static page generation for you. Most allow hybrid options with combining SSG, SSR, and even fancy approaches like incremental static regeneration.

Pros:

  • Simple to implement and maintain
  • Godlike UX
  • Awesome performance
  • Close to perfect SEO
  • Static data fetching
  • Easy migration to full SSR

Cons:

  • Needs a bunch of rewrite routing rules
  • Complex build step

Summary

So, that was a lot. ???? Just remember:

  • do static rendering as the default if your content is rather static
  • do full SSR if you want the absolute best performance and UX and have dynamic content
Search
Share
Featured articles
Generating SwiftUI snapshot tests with Swift macros
Don’t Fix Bad Data, Do This Instead