Explore by Category:
The cache is one of the most powerful weapons you can have when you want to make your web application fast. Delivering static, pre-rendered pages from the closest location can result in great performance, but setting it up on the server without making your frontend application ready to be cached can lead to unpleasant outcomes.
In the best-case scenario, you will annoy your users by breaking the app. Worst-case scenario, you will violate GDPR rules. This is certainly a situation that none of us wants to experience!
Don't worry - from this article, you will learn everything you need to know to make your Server-Side or Statically generated Single Page Applications ready to be cached (and hopefully distributed through a CDN)!
Side note: I use Nuxt.js in code examples, but the concepts and solutions are not tied to any framework.
We cache HTML templates to improve performance
Let's learn some theory first and familiarize ourselves with the concept of full-page caching. It's rather self-descriptive but let's ensure we understand it right by checking the definition. I found a very good one on Branch CMS documentation:
"Full page cache" means that the entire HTML output for the page will be cached. Subsequent requests for the page will return the cached HTML instead of trying to process and re-build the page, thus returning a response to the browser much faster
Generating the static HTML file on the server can take a few seconds. A few seconds when your user sees a blank screen and potentially leaves your website.
According to Google's study , 1-3 seconds of load time increases the bounce rate probability by 32%!
To deal with this significant performance downside of Server-Side Rendering, developers started caching the first generated response and sending it to others. Thanks to that, we perform the time-consuming rendering step only once and send its outcome immediately to all users requesting it.
The cached template has to be generic
Because of that you need to ensure that there is no session-specific content in both template and application state after server-side code execution. Otherwise, it will be cached and served to all users.
A session-specific content contains data and markup that can be different depending on the user or device - for example: displayed user or device name, conditional rendering statement for specific devices etc.
If you don’t remove session-specific content from the cached template, everyone will see a page with the data of the first user.
Execute session-specific content in the browser
Our applications are rarely completely generic in the real world, though. There is nothing wrong with this - almost every website has some content that is dynamic and depends on a current user session.
It’s important to make the dynamic content rendering happen in an environment that is isolated for each user - their browser.
This way the generic content, generated on the server is distributed immediately to all of your users and then, in their browsers, parts that make the experience tailored specifically for them are injected.
How to deal with session-specific templates
The first thing that comes to mind when thinking about session-specific content is the one of the currently authenticated user. We obviously don't want other users to receive a page that is filled with someone else's data.
Let’s see an example of excluding session-specific parts of an AppHeader component from rendering on the server.
In Nuxt.js you can skip the server-side rendering of components wrapped with a built-in
The server-side-rendered code of the above component will look more or less like this:
Then in the browser, the rest of the elements are added dynamically.
Improving the user experience with loading indicators and placeholders
Usually, the hydration happens in milliseconds, but on some devices, it could take even more than 10 seconds. Seeing empty elements on the template can be misleading and deliver a bad user experience. Users can feel that the website is broken, or they could miss some important elements that are loaded later. We should let them know that some elements are not there yet.
The most common and effective technique of indicating yet-to-be-loaded content are skeletons.
Skeleton screens *are blank pages that are progressively populated with content, such as text and images, as they become available.
Skeletons are much better option than spinners because they give users a hint of what content they can expect.
Let's take a look at the practical example of Linkedin. When you enter the page, not everything is loaded yet. You immediately receive a cached skeleton home screen, and the data is loaded progressively in the browser:
The Nuxt.js ClientOnly component has a placeholder slot we can use to display a skeleton for the content that will gradually load on the client side. I used SfSkeleton component example from Storefront UI - eCommerce UI library we’ve built in Vue Storefront for our users.
💡 Sometimes it is better to just use the generic state of the component as a placeholder
What about the data?
Until now we have talked only about the session-specific templates but what about data fetched inside components?
Most of the SSR frameworks like Nuxt or Next send the server-side state at the bottom of index.html file that comes with the rendered HTML, so you don’t have to fetch the data twice - on the server and then on the client.
If you inspect your Nuxt or Next SSR response, you will see a similar piece of code injected at the end of the body tag:
Because of that, we have to ensure that no session-specific data is in the cached HTML just like we did with the templates.
The data can be fetched from two places:
Inside the component
When the data is fetched inside the component, the case is simple. If the whole component is wrapped with ClientOnlylike in the example above the execution of the component code is completely skipped on the server, so the data is fetched for the first time when its executed in the browser.
If you’re REST APIs and have multiple requests on your page, executing the fetching logic inside the components is best. It’s much harder to miss the session-specific data if it’s not all around your app.
In the parent component
If you follow a smart/dumb components pattern aka. presentational/container (explained well by Dan Abramov here , and no longer a go-to approach since the introduction of React Hooks/Composition API) and fetch all the logic in the parent component, you have to take care of the session-specific data separately in the parent component.
To make your app cacheable and CDN-ready you have to avoid session-specific content in the cached content rendered on the server.
You have to render your application in a few steps - a generic one that renders most of the page and a session-specific one that injects dynamic parts.