An HTTP Caching Strategy for Static Assets: Generating Static Assets

This post is the second in a four-part series:

  1. An HTTP Caching Strategy for Static Assets: The Problem
  2. An HTTP Caching Strategy for Static Assets: Generating Static Assets

In Adobe Experience Platform Launch, the absence of a caching strategy is a bad caching strategy. We want most of our files to be cached as long as possible. Even so, when we release an update, we want to make sure the user gets the update. These goals might seem somewhat contradictory. If the files are always pulled from cache after the initial load, how will the browser ever know if there are updates on the server? Fortunately, there are ways to accomplish both goals.

When a user visits our Launch application, the first thing the browser requests is index.html, an HTML file. For our discussion, let’s assume the content is as follows:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="/styles.css" rel="stylesheet">
</head>
<body>
Content
<script src="/app.js"></script>
</body>
</html>

For index.html, we’ll configure our server to send an HTTP cache header that tells the browser to go ahead and cache index.html, but before the browser uses the cached copy in the future, it must check with the server and receive a response confirming that the browser’s copy is the latest available. There is some overhead in this check, but it’s a necessary cost.

In our example, the browser will then load two additional files: styles.css and app.js. For these files, we’ll configure our server to send an HTTP cache header that tells the browser to cache the files for as long as possible and the browser should always use the cached copy in the future without any need to check with the server first.

We’ll discuss how to configure these cache headers later.

Now, let’s say we need to release an update, but we’ve only made changes to the content of app.js. The trick here is to change the filename from app.js to, say, app2.js. Because we’ve changed the filename, we also need to update our index.html content to reference app2.js instead of app.js:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="/styles.css" rel="stylesheet">
</head>
<body>
Content
<script src="/app2.js"></script>
</body>
</html>

The next time a user visits Launch, the browser will check with the server to determine if the cached index.html is the latest available. The server will respond that the browser’s cached copy is not the latest available and will also attach the latest index.html in the response. The browser will read the updated index.html and see that it needs to load two files: styles.css and app2.js. The browser will find styles.css in its cache and, as instructed previously, use it without any additional check with the server. Since cache lookup is by filename, the browser will not find app2.js in its cache and therefore request it from the server.

As you can see, we’ve tried to leverage caching as much as possible while also ensuring the user has the latest code.

Automatically updating files based on content

The process described above can be done manually, but it’s a bit tedious and prone to error. For our user interface, we use Webpack to bundle our assets. Webpack takes assets as inputs (JavaScript files, CSS files, etc.), transforms or bundles them depending on configuration, and outputs files. The output filenames can be configured to include a hash based on the contents of the output file. We call these “content-addressed” filenames (the names are based on the file’s content).

Let’s take a look at a very simple Webpack configuration to see how this is done:

const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].cache-[chunkhash].js',
path: path.resolve(__dirname, ‘dist’)
}
};

When we run Webpack, it will take index.js as an entry file, perform any bundling, and output a file with a name like main.cache-cb1aa1a4fbfff0c1518c.js. Notably, if we run Webpack again without changing our source files, the output name will be exactly the same. This is because cb1aa1a4fbfff0c1518c is a hash of the file content. Given the same input file content, it will always produce the same output hash. If we were to change the content of our source files, the output filename would change. This is exactly what we want.

That takes care of renaming our file when content changes, but we still need to update our index.html file to load the newly-named JavaScript file. This can be done using the HTML Webpack Plugin, which will output an HTML file with a <script> tag that will load our newly-named JavaScript file:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin()
],

output: {
filename: '[name].cache-[chunkhash].js',
path: path.resolve(__dirname, 'dist')
}
};

I’ve highlighted the additions to our Webpack configuration. When we run Webpack now, it will produce a file named index.html with the following content:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
</head>
<body>
<script type="text/javascript" src="main.cache-84433f6cfd76536d22f1.js"></script></body>
</html>

Notice that it automatically added a <script> tag that will load the JavaScript file that was also generated.

You’ll most likely want to customize the generated HTML, which HtmlWebpackPlugin lets you do in numerous ways. For Launch, we created a custom template HTML file that we run through HtmlWebpackPlugin using the template option.

In the example above, we’ve only shown how to deal with a JavaScript asset, but Webpack will allow you to handle stylesheets or other assets in a similar manner.