Client-side vs. Server-side vs. Pre-rendering for Web Apps
There’s no question that user experience is impacted by perceived load times. With today’s heavier front ends, client-side rendering doesn’t feel very fast. In this article, Toptal Freelance Front-end Developer Guillaume Breux compares client-side, server-side, and pre-rendering strategies to help you choose the best option for your own app.
There’s no question that user experience is impacted by perceived load times. With today’s heavier front ends, client-side rendering doesn’t feel very fast. In this article, Toptal Freelance Front-end Developer Guillaume Breux compares client-side, server-side, and pre-rendering strategies to help you choose the best option for your own app.
Guillaume’s degree in photography and graphic design experience give his front-end web development the striking edge of beautiful imagery.
Expertise
There is something going on within the front-end community recently. Server-side rendering is getting more and more traction thanks to React and its built-in server-side hydration feature. But it’s not the only solution to deliver a fast experience to the user with a super fast time-to-first-byte (TTFB) score: Pre-rendering is also a pretty good strategy. What’s the difference between these solutions and a fully client-rendered application?
Client-rendered Application
Since frameworks like Angular, Ember.js, and Backbone exists, front-end developers have tended to render everything client-side. Thanks to Google and its ability to “read” JavaScript, it works pretty well, and it’s even SEO friendly.
With a client-side rendering solution, you redirect the request to a single HTML file and the server will deliver it without any content (or with a loading screen) until you fetch all the JavaScript and let the browser compile everything before rendering the content.
Under a good and reliable internet connection, it’s pretty fast and works well. But it can be a lot better, and it doesn’t have to be difficult to make it that way. That’s what we will see in the following sections.
Server-side Rendering (SSR)
An SSR solution is something we used to do a lot, many years ago, but now it frequently loses in the server-side rendering versus client-side rendering battle, as we tend to forget it in favor of a client-side rendering solution. Here, we’ll discuss when to use server-side rendering and how this method works.
With old server-side rendering solutions, you built a web page—with PHP for example—the server compiled everything, included the data, and delivered a fully populated HTML page to the client. It was fast and effective.
But… every time you navigated to another route, the server had to do the work all over again: Get the PHP file, compile it, and deliver the HTML, with all the CSS and JS delaying the page load to a few hundred ms or even whole seconds.
What if you could do the first page load with the SSR solution, and then use a framework to do dynamic routing with AJAX, fetching only the necessary data?
This is why SSR front-end solutions are getting more and more traction within the community because React popularized this problem with an easy-to-use solution: The RenderToString
method.
This new kind of web application is called a universal app or an isomorphic app. There’s still some controversy over the exact meanings of these terms and the relationship between them, but many people use them interchangeably.
Anyway, the advantage of this solution is being able to develop an app server-side and client-side with the same code and deliver a really fast experience to the user with custom data. The disadvantage is that you need to run a server.
SSR is used to fetch data and pre-populate a page with custom content, leveraging the server’s reliable internet connection. That is, the server’s own internet connection is better than that of a user with lie-fi), so it’s able to prefetch and amalgamate data before delivering it to the user.
With the pre-populated data, using an SSR app can also fix an issue that client-rendered apps have with social sharing and the OpenGraph system. For example, if you have only one index.html
file to deliver to the client, they will only have one type of metadata—most likely your homepage metadata. This won’t be contextualized when you want to share a different route, so none of your routes will be shown on other sites with their proper user content (description and preview picture) that users would want to share with the world.
Pre-rendering
The mandatory server for a universal app can be a deterrent for some and may be overkill for a small application. This is why pre-rendering can be a really nice alternative.
I discovered this solution with Preact and its own CLI that allows you to compile all pre-selected routes so you can store a fully populated HTML file to a static server. This lets you deliver a super-fast experience to the user, thanks to the Preact/React hydration function, without the need for Node.js.
The catch is, because this isn’t SSR, you don’t have user-specific data to show at this point—it’s just a static (and somewhat generic) file sent directly on the first request, as-is. So if you have user-specific data, here is where you can integrate a beautifully designed skeleton to show the user their data is coming, to avoid some frustration on their part:
There is another catch: In order for this technique to work, you still need to have a proxy or something to redirect the user to the right file.
Why?
With a single-page application, you need to redirect all requests to the root file, and then the framework redirects the user with its built-in routing system. So the first page load is always the same root file.
In order for a pre-rendering solution to work, you need to tell your proxy that some routes need specific files and not always the root index.html
file.
For example, say you have four routes (/
, /about
, /jobs
, and blog
) and all of them have different layouts. You need four different HTML files to deliver the skeleton to the user that will then let React/Preact/etc. rehydrate it with data. So if you redirect all those routes to the root index.html
file, the page will have an unpleasant, glitchy feel during loading, whereby the user will see the skeleton of the wrong page until it finishes loading and replaces the layout. For example, the user might see a homepage skeleton with only one column, when they had asked for a different page with a Pinterest-like gallery.
The solution is to tell your proxy that each of those four routes needs a specific file:
-
https://my-website.com
→ Redirect to the rootindex.html
file -
https://my-website.com/about
→ Redirect to the/about/index.html
file -
https://my-website.com/jobs
→ Redirect to the/jobs/index.html
file -
https://my-website.com/blog
→ Redirect to the/blog/index.html
file
This is why this solution can be useful for small applications—you can see how painful it would be if you had a few hundred pages.
Strictly speaking, it’s not mandatory to do it this way—you could just use a static file directly. For example, https://my-website.com/about/
will work without any redirection because it will automatically search for an index.html
inside its directory. But you need this proxy if you have param urls—https://my-website.com/profile/guillaume
will need to redirect the request to /profile/index.html
with its own layout, because profile/guillaume/index.html
doesn’t exist and will trigger a 404 error.
In short, there are three basic views at play with the rendering strategies described above: A loading screen, a skeleton, and the full page once it’s finally rendered.
Depending on the strategy, sometimes we use all three of these views, and sometimes we jump straight to a fully-rendered page. Only in one use case are we forced to use a different approach:
Method | Landing (e.g. / ) | Static (e.g. /about ) | Fixed Dynamic (e.g. /news ) | Parameterized Dynamic (e.g. /users/:user-id ) |
---|---|---|---|---|
Client-rendered | Loading → Full | Loading → Full | Loading → Skeleton → Full | Loading → Skeleton → Full |
Pre-rendered | Full | Full | Skeleton → Full | HTTP 404 (page not found) |
Pre-rendered With Proxy | Full | Full | Skeleton → Full | Skeleton → Full |
SSR | Full | Full | Full | Full |
Client-only Rendering is Often Not Enough
Client-rendered applications are something we should avoid now because we can do better for the user. And doing better, in this case, is as easy as the pre-rendering solution. It’s definitely an improvement over client-only rendering and easier to implement than a fully server-side-rendered application.
An SSR/universal application can be really powerful if you have a large application with a lot of different pages. It allows your content to be focused and relevant when talking to a social crawler. This is also true for search engine robots, which now take your site’s performance into account when ranking it.
Stay tuned for a follow-up tutorial, where I will walk through the transformation of an SPA into pre-rendered and SSR versions, and compare their performance.
Further Reading on the Toptal Blog:
- Why the Hell Would I Use Node.js? A Case-by-case Tutorial
- Creating Server-side Rendered Vue.js Apps Using Nuxt.js
- Optimizing Website Performance and Critical Rendering Path
- Top 10 Mistakes That Django Developers Make
- React SEO Strategies and Best Practices
- Next.js vs. React: A Comparative Tutorial
- Advantages of AI: Using GPT and Diffusion Models for Image Generation
Understanding the basics
What is client-side rendering?
Client-side rendering allows developers to make their websites entirely rendered in the browser with JavaScript. Instead of having a different HTML page per route, a client-side rendered website creates each route dynamically directly in the browser. This approach spread once JS frameworks made it easy to take.
What is the server-side rendering?
Server-side rendering allows developers to pre-populate a web page with custom user data directly on the server. It is generally faster to make all the requests within a server than making extra browser-to-server round-trips for them. This is what developers used to do before client-side rendering.
What is the difference between client-side and server-side rendering?
Client-side rendering manages the routing dynamically without refreshing the page every time a user requests a different route. But server-side rendering is able to display a fully populated page on the first load for any route of the website, whereas client-side rendering displays a blank page first.
What is pre-rendering?
Pre-rendering is a tradeoff between client-side and server-side rendering. Every pre-rendered page displays a skeleton template while the data waits to be rehydrated with AJAX/XHR requests. Once the page is fetched, internal routing is done dynamically to take advantage of a client-side rendered website.
What is a universal app?
A universal app sends to the browser a page populated with data. Then the app loads its JavaScript and rehydrates the page to get a fully client-side rendered app. This approach combines the advantages of the latest techniques available today.
La Massana, Andorra
Member since July 23, 2018
About the author
Guillaume’s degree in photography and graphic design experience give his front-end web development the striking edge of beautiful imagery.