Frontend Caching Quick Start

A Quick Start Guide to Frontend Caching

🏎️ In the race to send the last bits needed to view a web page, we need to position ourselves to be as close to the finish line (browser) as possible! 🏁

The primary goal related to setting up a caching strategy on your client side web application is to ensure the browser fetches the latest version of a respective file only if required.

Let’s work through different caching scenarios as we build a web app from the ground up. In the last section, we provide an easy framework for you to derive an optimal caching strategy for any application!

This tutorial is primarily for learning purposes - providing fundamental building blocks of an optimal caching strategy. It provides a guided method to help anyone take the first steps towards either setting up or modifying an application’s caching strategy based on cache control headers specifically. Advanced topics such as service workers, CDNs, etc. are out of scope.

The Pet Web App

Imagine we have a simple Pet Web App that shows an image of a dog and cat.

web example

To start off, we assume file versioning is not set up for any part of our project. Any modifications will overwrite the exact file. As we visit the next steps, we setup versioning, and then finally we add server side rendering. We’ll discuss the important caching policies each step might require for best performance.

First, here’s the project resource tree for our Pet Web App:

Pet Web App
│
├── main.js
├── style.css
│
├── index.html
│
├── images
│   ├── dog.jpg
│   └── cat.jpg
│
└── healthcheck.html

The following image shows a graph of each html file and the resource(s) they reference:

index references

Now, each file part from our resource map has certain characteristics about its content.

We’ll focus on the following characteristics: Mutable and Immutable.

The type of content of each file being served is important to understand, as it allows us to think about the optimal caching behavior.

Mutable Content

Immutable Content

immutable

Before we can discuss the optimal caching strategy for our project, we’ll map which files in the project resource tree are immutable and which are mutable:

After mapping out the each resource’s mutability or immutability, we can now choose a caching strategy and server configuration.

Caching Strategy

Side Note: If your server configuration does not allow for ETags, then you should switch to having browsers never cache the index.html file. This can be done by leveraging the no-store cache control directive instead of no-cache.

The following diagram shows the flow between the browser and server for the scenario where the resource has not changed.

not modified

On the other hand, if the file has newer content, the following flow between the browser and server occurs.

modified

Side Note: The HTTP 1.1 Specification states: “To mark a response as ‘never expires’, an origin server sends an Expires date approximately one year from the time the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more than one year in the future.” Hence, the recommended max-age value for a resource which never expires is 1 year. By setting max-age: 31536000, we’re telling the client to cache it for up to 31536000 seconds, which is 1 year from the time of the request.

The following diagram shows the browser and server flow for the no-store scenario.

no store

The Pet Web App project currently is a very basic application without versioning for our source code files. Yet, we do have an optimal caching policy which prevents unnecessary downloads of the project resources by the browser.

However, we can do better. We can set up versioning for our source code files such that the client will never have to ask the server to validate the currently cached copy of the resource. It will always use what it has cached. For this, let’s head to the next section.

Versioning For Our Resources

Let’s map out how we’ll leverage versioning for each resource to improve our caching strategy.

The following diagram depicts how index.html should be updated to always point to the latest generated main.[contenthash].js file. The same flow should occur for the style.css file.

source iterations

Side Note: If you use a bundler like webpack you can have it generate files with content hashes included in their names.

With our versioning criteria set, we see that the mutability of our Javascript and CSS files has now changed.

Finally we can update the caching policy based off the new content charactistic of our Javascript and CSS files.

Caching Strategy Update

Big picture

The following diagram depicts how index.html will refer to the newer resources after changes:

source iterations

Server Side Rendering

Let’s say we now want to optimize our site for performance by leveraging server-side rendering (SSR). We want to pre-render some of our content. As a result, we won’t have an index.html file anymore. We’ll have an index API route, e.g. /, which responds to requests. Everything else in the project remains the same.

Let’s look at the content characterisitic of our index API route:

Based off the content characteristic of the resource, we can generate an optimal caching policy:

But wait! I already set Cache-Control headers for my resources, how should I update them?

If you already set Cache-Control headers for your resources and want to update them, then you have a few options, depending on the content characteristic of the resource. Regardless of content characteristic, your first step will be updating the Cache-Control headers. Your next move depends on whether the resource is mutable or immutable:

Mutable

In the places where this resource is referenced, append a dynamic query string parameter to the resource’s url. The browser will be forced to fetch the latest version of the resource, and in turn, it will also get the latest value of the respective Cache-Control header. Example:

Before: <script type="text/javascript" src="my-file.js"></script>

After: <script type="text/javascript" src="my-file.js?version=2"></script>

If you don’t want to append a query parameter, another option would be to place the resource under a new versioned folder, e.g. /v2/my-file.js.

A Cache-Control header on index.html presents a unique problem. It’s a mutable resource that can’t be versioned, so how can we propagate the new header? If your web application is behind a CDN or a hosting provider then you can leverage the provider’s capabilities for flushing the cache, if that feature is supported. However, in the worst case, you can try the following if you have analytics for your web application: - Update the cache control header and add a tracking code to index.html. - Analyze how many users are hitting the new index.html vs the older index.html. - This provides an overview of how your new content is being propagated across all your users. - From this, you can set a threshold of how many users should be visiting the newer index.html over a span of N days. - If the threshold is reached, then you’re set. If not, and the Cache-Control header is set to a long period of time, then you’re pretty much at the mercy of the browsers. Hence, the Cache-Control header for index.html is very important to set correctly from the start!

Immutable

If the resource is immutable, then no changes should be needed. As discussed in the immutable resource portion of this post, edits to the source files generate new corresponding build files with unique identifiers. In this case, the html files referencing the resource would be updated to request the new version, resulting in browsers downloading the new files along with the newly set Cache-Control headers, as long as the html file itself had appropriate Cache-Control headers.

Easy 3 Step Process Toward An Optimal Caching Strategy

Notice that there is a pattern emerging for developing an optimal caching strategy. Here’s how you can compute an optimal caching strategy for a web application:

  1. First create a project resource map.
  1. Determine the content characteristic of each resource in the project resource map. Is the resource immutable or mutable ? If the resource is mutable, can it benefit from being converted to an immutable resource with a proper versioning scheme?

  2. Based off the content characteristic of each resource you can now create an optimal caching strategy.

Conclusion

We hope this post helps you create an optimal caching strategy for your application. We focused primarily on the Cache-Control header, but there are other ways to improve browser caching and the delivery speed of your web application, such as leveraging service workers or a Content Delivery Network (CDN). Good luck and happy caching! 🏎️

Attributions


Author