Blend a million renders into 2 thanks to the HTTP cache

Treat your users right with HTTP Cache and Segmented Rendering

Eric Burel
VulcanJS

--

This article is long (10–15 minutes), but worth it! Grab yourself a hot drink and enjoy the read.

🇫🇷 Speak French? Cet article est disponible en français sur PointJS.

The rich guest and the poor customer

Picture yourself browsing your favorite online magazine. For me, that would be a blog describing obscure expensive music synthesizers I will never actually buy.

The performances of the website are marvelous. Because the content is public and shared among all users, it can be cached somewhere near you, at the “edge”, and sent as is in HTML.

Imagine this website has a paying section, for really really good articles. You buy a subscription and proceed to log in.

Suddenly, everything starts to be slower. You see awful spinners everywhere. Your CPU becomes noisy. You wait 3 second for each page to load. You can fry an egg on your Ipad.

That’s because you left the wonderful world of statically rendered content. Now, it’s client-side rendering everywhere! Per-request server-side rendering all around!

Just because you are now a unique, special, authenticated user, reading private content you paid for.

This is what I call the rich guest and the poor customer problem. Guests are enjoying optimal performance, with static or cached rendering, while authenticated users get the worst, with per-request SSR and client-side rendering. Doesn’t make much sense!

Introducing Segment Rendering: 1 segment = 1 render

Marketing divides people into homogeneous segments. Guests are one segment, paid Customers are another segment.

Only the customer segment can see the paid content, while guests should be redirected to a subscription page.

There are many other scenarios where you can define segments that should see some specific content on your website:

  • A/B testing: all users of a bucket will see the same variation of the website
  • Multi-tenancy: all users of a given company will see the same logo and color scheme
  • Internationalized content: all users with the same language see the same texts
  • Dark/light theme : all users that picked light mode are destroying their cornea

In these examples, all users of a segment should see the same content. Cached or static rendering for each segment would be awesome!

Let’s call this pattern “Segmented Rendering”.

Our goal here is to do only one render per segment. This will both reduce the cost of operating our website, and improve the perceived performance for end-users.

Sadly, many “Jamstack” frameworks are supposing that if a content is not public and the same for everybody, then it cannot be statically rendered. They won’t let you implement Segmented Rendering easily.

But let’s discover 2 patterns to bypass this limitation.

A non-standard implementation with Next.js

I’ve already described a full-fledged implementation of this “Segmented Rendering” pattern with Next.js. I also proved formally, with some maths, that this pattern achieves a minimal number of renders, basically pushing static rendering to its most extreme boundaries. And I’ve sent a PR to Next.js documentation to clarify the conditions where SSG is actually applicable.

The problem is that this approach relies on non-standard features, Next.js middlewares and SSG/ISR specifics. In Remix.js, you can cache renders using some custom logic in entry.server. It can be extended to other Jamstack frameworks using a proxy server, but not out-of-the-box.

To sum it up, a server must be in charge of verifying the request content and redirecting to the right variation of the page.

Let’s try to achieve Segmented Rendering using only one standard technology: the HTTP cache.

Small technical note: I tend to use “static” and “cached” interchangeably in this article. That’s because static rendering is roughly equivalent to caching some server renders at build-time or even on first request. Putting a well-configured cache in front of any framework that can do per-request server-side rendering turns it into a static framework.

A standard implementation with HTTP cache

A pillar of the Remix framework philosophy is “working with the foundations of the web browser“. The HTTP protocol indeed comes with a powerful cache system. Mozilla documentation is a great place to start if you want to learn more about it. Let’s use this cache to implement Segmented Rendering.

1. Enable per-request Server-Side Rendering

We need a way to render the HTML for each variation of your page, depending on the user’s request. For instance, one version per language.

The first step is to setup your application to enable request-time SSR:

  • In Next.js via getServerSideProps; in Gatsby via the getServerData; in Remix it’s the default behavior.

2. Identify segments via the HTTP request

Given a user’s request, we must guess their segment. For instance, their preferred language.

Second step is clarifying the segments of your app:

  • Identify segments via elements of the incoming request: usually cookies, URL parameters, and sometimes specific headers like “Accept-Language”

3. Setup the Vary header of the HTTP response

Now, we must actually configure the cache, so that we store/retrieve one version of the page per segment. For instance, the first time a French user visits our website, we render the French version and cache it. The second French user will receive the cached version immediately.

Third step is to tell the HTTP cache how to distinguish your segments:

Example: Vary: Cookie, Accept-Language

Final step: setup the Cache-Control header of the HTTP response

Example: Cache-Control: public, s-maxage=604800

The “maxage” is in seconds. Be careful to setup this duration according to your content lifespan. For instance a recent article might be edited after being published, so keep it low, and then increase it for older content that barely move anymore. The storage must be “public”: the point of segmented rendering is to share the same render between multiple users.

You’re done!

By using this simple approach, the server-side render will happen only once per segment!

This is not a “static” render in the traditional meaning, because the render doesn’t happen at build-time, but at first render. This is actually closer to Next.js “ISR”. But the number of renders is actually exactly the same as static: 1 for each segment.

You get most of the customization possibilities of per-request SSR, with the performances of build-time static site generation!

Bonus: ETags to version each variation

The “ETags” header let’s you assign a specific version to some contents. Here, the ETags should contain an unique identifier for the segment. Don’t forget to also add a unique version identifier, for instance the latest Git commit of your repository.

Example: ETag: bucket-b-theme-light-commit-1234

This header will be handled only by the browser. It is useful if the user tend to change its segment a lot, for instance when trying various themes in their settings.

Limitations of the HTTP cache for Segmented Rendering

Sadly, this standard pattern with the HTTP cache is actually inferior to our “non-standard” Next.js-based implementation. Here is why.

App-level and CDN-level limitations

The HTTP cache is handled by the Content Delivery Network. You can setup your own CDN, or rely on the CDN provided out-of-the-box by your hosting provider. If you were attentive in the last section, you might have noticed multiple limitations:

  • If your framework doesn’t enable per-request SSR, you won’t be able to use the HTTP cache for Segmented Rendering. Sorry. However, a custom implementation would still work, as long as you can put a redirection server in front of your app. See my article for a solution that works everywhere.
  • Be careful that the Vary header is currently (05/2022) not supported by some major CDN, including Vercel. Sorry. If you cannot use a CDN that supports this header, move back to the non-standard implementation.
  • Each CDN may have some additional security and privacy rules. For instance, Vercel will not fill the cache if the request contains the Authorization header, for privacy reasons. Segmented rendering may not work if you log in for instance via Basic authentication, even for public content. Sorry again.
  • You cannot pick the cookie key. This old article describes the proposal for an additional Key header, that do not seem to have been implemented since. Really sorry!

What about paid and private content?

HTTP caching has 2 modes:

  • public, cached render is stored on the CDN, available to anyone
  • private, cached render is stored on the user’s browser

There is no such thing as a private render on the CDN, because you usually cannot do computations in the CDN. You cannot check authentication, you cannot verify that the user has a paid subscription or belongs to the right tenant or A/B tests.

Segmented rendering with the HTTP cache won’t work for our blog with a paid section. There is no other option than implementing a server-side cache. This approach gives you more control on the access permissions and works for private/paid/secure content.

A custom Server cache is always more flexible than a HTTP cache

To put it in a nutshell, under certain conditions, the HTTP cache allows you to go further than traditional static rendering. You can have multiple variations of the same page. It applies to use cases such as internationalized content, some simple A/B tests, theming, public pages of each tenant. Especially if the variation is selected via an URL parameter. It’s already a lot!

But if you want more, the only solution is to put a proxy server in front of your application, and rely on some kind of server-side caching. It can be either static rendering or SSR with an in-memory cache or whatever you come up with. The article I already shared above show you how to do that with Next.js, check it out if you want to learn more about Segmented Rendering.

You should now be able to make your customers rich again, and your guests… even richer!

Many thanks to Sergio Xalambrí (sergiodxa) for sharing is incredible knowledge of HTTP caching with me. If you enjoyed this article, you will most definitely enjoy his blog: https://sergiodxa.com/

Thanks for reading this article! If you liked it, please take a minute to discover our Next.js and GraphQL framework, VulcanNext.

Click to discover Vulcan Next, the Next.js GraphQL framework

Subscribe to Vulcan’s blog for more stories like this or follow me on Twitter: @ericbureltech

--

--

Eric Burel
VulcanJS

Next.js teacher and course writer. Co-maintainer of the State of JavaScript survey. Follow me to learn new Next.js tricks ✨ - https://nextjspatterns.com/