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:
- Initial markup is what user sees while scripts load and start up
- Loading state refers to the state when actual data is being fetched after the app has bootstrapped
- 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.
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:
https://gist.github.com/2f70505f7225e0c501c46b1741213e44
The initial page is usually blank, maybe with customized background color. 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 summarized 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
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! ๐
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
- Some SEO
Cons:
- Wasteful server work
- User still waits for content
- Mediocre SEO
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. :money_with_wings:
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 server, and a damn complex at that. On top of that, getting data serialization and hydration working is quite tedious and prone to errors.
Here is a typical flow for a single request:
- Collect necessary data requirements of the request
- Fetch the data from the server
- Render markup with the fetched data
- Serialize data and embed them into the HTML
- On the client, deserialize data and supply them to the initial render
- Bulletproof the initial render to be 100% consistent with the server render
- Use the data from the server and avoid double-fetching ๐ฅต
So go ahead, whip out your fancy dancy SSR with content going on, just make god damn sure you can handle it!
Pros:
- Godlike UX
- Top performance
- Perfect SEO
- Server-side data fetching
Cons:
- Insane complexity! ๐ฅ
- Gotta have a server
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:
- SSR with data fetching
- 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.
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 favor simplicity over optimization, this is the approach for you. ๐
Pros:
- No server! ๐
- Simple to implement and maintain
- User immediately sees something
- Seamless initial to loading state transition
- Some SEO
Cons:
- User still waits for content
- Mediocre SEO
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 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
or404.html
- static URLs with content, like
/about
- loading skeleton for dynamic fallback URLs, like
/program/:id
, in case:id
is not pre-rendered - 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.
Pros:
- No server! ๐
- 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
So, that was a lot. ๐ Just remember:
- do static rendering as the default, full with less dynamic content, and plain with more dynamic content
- do full SSR if you want the absolute best performance and UX and give zero fucks about anything else
Cheerio! ๐