From Web Performance in Action by Jeremy Wagner

In this article, we’ll learn what caching is, how to develop a caching strategy that’ll provide the best performance for your website, as well as how to invalidate cached assets when you update your website’s content. Let’s begin by learning how caching works.

Save 37% on Web Performance in Action. Just enter code fccwagner into the discount code box at checkout at

The first impression of your website’s the most important. You should optimize with the assumption that any one visit to your website’s the first time that particular user has been to your site. A bad first impression may be enough to prevent someone from returning.

This is a good assumption to operate under, it’s also important to remember that a significant portion of your users are also returning visitors, or are navigating to subsequent pages. These are scenarios where your site will benefit from a good caching policy. Of course, a business can always take advantage of the services of expert web designers such as those from The Web Designer Cardiff (you can get it from here) who can take care of the technical and maintenance aspects of the business website.

Understanding Caching

Caching is a simple concept. When the browser downloads an asset, it follows a policy dictated by the server to figure out whether or not it should download that asset again on future visits. If a policy isn’t defined by the server, browser defaults will kick in, which is usually to cache files for that session. This process is illustrated in Figure 1.


Figure 1 – A basic overview of the caching process. The user requests index.html, and the server checks if the asset has changed since the time the user last requested it. If the asset hasn’t changed, the server responds with a 304 Not Modified status and the browser’s cached copy is used. If it has changed, the server responds with a 200 OK status along with a new copy of the requested asset.

Caching’s a powerful performance enhancement that has an immense effect on page load times. To see this effect, open Chrome’s developer tools, go to the network panel, and disable caching. Then go to the Weekly Timber website you downloaded from Github earlier in this chapter and load the page. When you do, take note of the load time and the amount of data transferred. Then, re-enable caching, reload the page and take note of the same data points. You’ll see something similar to what’s in Figure 1.


Figure 2 – The load times and data payload of a website on the first uncached visit, and on a subsequent visit. The page weight’s nearly 98% smaller, and the load time’s much faster, all due to caching.

The behavior you see in the above figure can be broken down into two different cache states: Unprimed and primed. When a cache is unprimed, we’re referring to the first time that a user visits a site. This is when the user has an empty browser cache, and everything must be downloaded from the server to render the page. The primed state’s when the user visits a page again. In this state, the assets are in the browser cache, and aren’t downloaded again.

Naturally, you’re curious as to what drives this behavior. The answer to your burning curiosity is the Cache-Control header. This header dictates caching behavior in nearly every browser in use, and its syntax is easy to understand. Before we go any further, let’s use git and node to work with an example website for Weekly Timber, a logging company from Central Wisconsin. To download this site, execute the following commands:

git clone cd ch10-asset-delivery

Once this has finished, you’ll need to install dependencies to be able to run the site locally with Node. To do this, enter npm install. Once the dependencies finish installing, you can start a webserver at http://localhost:8080 by typing node http.js. You can stop the web server at any time by hitting Ctrl+C. Let’s start by learning about the Cache-Control header’s max-age directive!

Using the Cache-Control Header’s max-age Directive

The easiest way to use Cache-Control is through its max-age directive, which specifies the life of the cached resource in seconds. A simple example of this is illustrated below:

Cache-Control: max-age=3600

For example, let’s say this response header’s set on an asset named behaviors.js. When the user visits a page for the first time, behaviors.js will be downloaded because the user doesn’t have it in their cache. On a repeat visit the requested resource is good for the amount of time specified in the max-age directive, which is 3,600 seconds (or, more intuitively, an hour.)

A good way to test this header is to set it to a low value, aroundten seconds. For our client’s website, you can specify a Cache-Control max-age value by modifying http.js, and editing the line that invokes the express.static call:

app.use(express.static(__dirname, {        maxAge: "10s" }));

The value of 10s is shorthand for “10 seconds.” When you start/restart the server with this modification, reload the page at http://localhost:8080. Then, rather than reload the page again, place your cursor into the address bar and hit Enter, or click on the logo at the top of the page that links to index.html. Look at the request for jquery.min.js in the network request listing and you’ll see something similar to Figure 3.


Figure 3 – A copy of jQuery being retrieved from the local browser cache.

When we navigate to a page, as opposed to reloading it (such as when you click the reload icon), the value of the Cache-Control header’s max-age directive influences whether or not the browser grabs something from the browser cache. If the item’s present in the cache, a request’s never made to the server for that item.

If you reload, or the time specified in the max-age directive elapses, the browser will contact the server to revalidate the cached asset. When it does this, it will check to see if the asset has changed. If it has, a new copy of the asset is downloaded. If not, the server will respond with a 304 Not Modified status without sending the asset. This process is illustrated in Figure 4.


Figure 4 – The effect of the Cache-Control header’s max-age directive and the browser/server interaction that results in its use.

The way the server checks to see if an asset has changed can vary. A popular method uses what’s called an Entity Tag, or ETag for short. This is basically a checksum generated from the contents of the file, and the browser sends this value to the server which validates it to see if the asset has changed. Another method checks the last time the file was modified on the server, and serves a copy of the asset based on its last modification time. You can modify this behavior with the Cache-Control header somewhat, and we’ll cover them briefly.

Controlling Asset Revalidation with no-cache, no-store and stale-while-revalidate

The max-age directive’s fine for most websites, but there may be times when you need to put limits on caching behavior, or abolish it altogether. A good example of this would be applications where data needs to be as fresh as possible, such as online banking or stock exchange sites. Three Cache-Control directives are at our disposal to help us limit caching behavior:

  • no-cache: This says to the browser that any asset downloaded can be stored locally, but that the browser must always revalidate the resource with the server, regardless of any max-age value present. max-age can be used in conjunction with this directive to enforce when a new copy of an asset should be downloaded, regardless of its freshness.
  • no-store: This directive goes one further than no-cache. no-store indicates that the browser isn’t to store the affected asset. This requires the browser to download any affected asset every time you visit a page.
  • stale-while-revalidate: This one’s nuanced, but also quite useful. Like max-age, stale-while-revalidate accepts a time measured in seconds. The difference is that when an asset’s max-age has been exceeded and becomes stale, this header defines a grace period that the browser’s allowed to use the stale resource in the cache. The browser should then fetch a new copy of the stale asset in the background and place it into the cache for the next visit. This behavior isn’t guaranteed, but it can boost cache performance in some limited scenarios.

Obviously, these directives affect or remove the performance benefits that caching provides, but there are times when you need to ensure that assets are never stored or cached. Use these directives sparingly and with good cause.

Cache-Control and CDNs

You might use a Content Delivery Network (CDN) in front of your site. If you don’t know what a CDN is, it’s a proxy service that sits in front of your site and optimizes the delivery of your content to your users. Figure 5 illustrates the basic CDN concept.


Figure 5 – The basic concept of a CDN. A CDN is a proxy that sits in front of your website, and distributes your content to users across the world. The CDN can do this through a network of geographically distributed servers that host your content. Users have their content requests fulfilled by servers that are closest in proximity to them.

The idea is that a CDN has the power to distribute your content across the globe. This means that your site assets and content can be served from computers that are closer in proximity to your users than if you served content solely from your own host. The shorter distance can result in lower latency for those assets, which in turn boosts performance.

In order to accomplish this, the CDN hosts your assets on their network of servers, which means that your content’s effectively cached by the CDN. Two Cache-Control directives can be used in combination with max-age called public and private, and they help you control how your content’s cached by CDNs.

Using a Cache-Control directive of public in conjunction with max-age is easily done:

Cache-Control: public, max-age=86400

This will instruct any intermediary (such as a CDN) to cache the resource on their server. You generally shouldn’t need to specify public if you’re using Cache-Control, because it’s implied, but being specific won’t hurt you.

The private directive’s used in the same syntax as public, but instructs any intermediary to not cache the resource. Using this header will treat the resource as if the CDN isn’t in play at all. It will pass the asset through to the user directly from the site’s web server. The user’s browser will still cache the resource according to the header’s max-age value, but only with respect to the origin web server behind the CDN, and not to the CDN itself.

From here, we’ll take all of this knowledge of the Cache-Control header and show you how to create a caching strategy that makes sense for your website.

Crafting an Optimal Caching Strategy

Now that you’ve all of this knowledge about the Cache-Control header, how do you apply it to your website? As with any new piece of knowledge, we’ll apply it to something practical, such as the Weekly Timber website. We’ll begin by categorizing assets, choosing a good max-age policy for each category as well as relevant directives that make sense. Then, we’ll go about applying it in our web server.

Categorizing Assets

When categorizing assets, the best parameter to use is how often an asset is likely to change. For example, HTML documents are likely to change often, and assets such as CSS, JavaScript and images are somewhat less likely to change.

The client’s website has rather basic caching requirements, which makes it a great introduction to using Cache-Control. The asset categorization for this website is simple: HTML, CSS, JavaScript and Images. The fonts are loaded via Google Fonts, and caching is handled by Google’s servers, leaving us with only the basics to consider. Table 1 is a breakdown of these asset types, and the caching policy I’ve selected for them:

Table 1 – Asset types for the Weekly Timber website, their modification frequencies, and the Cache-Control header value that should be used.

Asset Type

Frequency of Modification

Cache-Control Header Value


Potentially monthly, but needs to be as fresh as possible.

private, no-cache, max-age=3600

CSS and JavaScript

Potentially monthly

public, max-age=2592000


Almost never

public, max-age=31536000

The rationale behind these choices is nuanced, but easy to understand once you break it down by asset:

  • HTML files or server side languages that output HTML (e.g., PHP or ASP.NET) can benefit from a conservative caching policy. We never want the browser to assume that the page should be read only from the browser cache without ever revalidating its freshness.
    • no-cache ensures that the resource will always be revalidated, and if it has changed, a new copy will be downloaded. The revalidation of the asset does lessen the load on the server if the content of the file hasn’t changed, but no-cache never caches the HTML aggressively enough that content’s stale.
    • A max-age of one hour will ensure that no matter what, a new copy of the asset will be fetched after the max-age period expires.
    • Using the private directive tells any CDN in front of the origin web server that this resource shouldn’t be cached on their server(s) at all, only between the user and origin web server.
  • CSS and JavaScript are important resources, but don’t need to be aggressively revalidated. Therefore, we can use a max-age of 30 days.
    • Because we’d benefit from a CDN distributing this content for us, we should use the public directive to allow CDNs to cache the asset. If we need to invalidate a cached script or style sheet, we can do it easily, and that process is explained in the next section.
  • Images and other media files, such as fonts, rarely (if ever) change, and they’re often the largest assets you’ll serve. Therefore, a long max-age time (such as a year) is appropriate.
    • As with CSS and JavaScript files, we want CDNs to be able to cache this asset for us. Using the public directive makes sense here as well.

The caching strategy that’s best for your website may vary. You may decide that no caching should occur whatsoever with your HTML files, and this isn’t necessarily a bad approach if your site constantly updates its HTML content. In this case, it may be preferable for you to use the no-store directive, which is the most aggressive measure that assumes you never want to cache anything or bother with revalidation.

You may also decide that your CSS and JavaScript should also have a long expiration time like images. This is also fine, but unless you invalidate your caches, this could result in asset staleness when you make updates. You might go the other way and decide that the browser should revalidate a cached asset’s freshness with the server on every request. In any case, we cover how to properly invalidate your cached assets in the next section. For now, though, we need to implement our caching strategy on our Node web server!

Implementing the Caching Strategy

Putting our caching strategy into effect in the local web server is simple. We add a request handler that allows us to set response headers before assets are sent to the client. In this section, we’ll open http.js and use the mime module to inspect the types of assets requested, and set a Cache-Control header based on their type. Listing 1 shows the changed portions of http.js in bold, with annotations.

Listing 1 Setting Cache-Control headers by file type

var express = require("express"),        compression = require("compression"),        path = require("path"),        mime = require("mime"), â ¶          app = express();    // Run static server app.use(compression()); app.use(express.static(path.join(__dirname, pubDir), {        setHeaders: function(res, path){ â ·                  var fileType = mime.lookup(path); â ¸                     switch(fileType){ â ¹                          case "text/html": â º                                  res.setHeader("Cache-Control",                                                            "private, no-cache, max-age=" + (60*60)); â º                          break;                           case "text/javascript": â »                          case "application/javascript": â »                          case "text/css": â »                                  res.setHeader("Cache-Control",                                                            "public, max-age=" + (60*60*24*30)); â »                          break;                           case "image/png": â ¼                          case "image/jpeg": â ¼                          case "image/svg+xml": â ¼                                  res.setHeader("Cache-Control",                                                            "public, max-age=" + (60*60*24*365)); â ¼                          break;                }        } })); app.listen(8080);

â ¶ Imports the mime module that we’ll use to look up file types for requested assets.

â · The setHeaders callback allows us to specify behavior that’ll run before the response is sent.

â ¸ The file type of the requested asset is determined by the mime module.

â ¹ A switch statement’s run on the value of the fileType variable.

â º HTML files set Cache-Control: private, no-cache, max-age=3600

â » JavaScript and CSS files set Cache-Control: public, max-age=2592000

â ¼ PNG, JPEG and SVG images set Cache-Control: public, max-age=31536000

Once finished, start (or restart) the server. Testing cache policy changes is tricky, but here’s a three step process you can use in Chrome to see how things are working:

  1. Open a new tab and open the network panel. Make sure the “Disable cache” box is checked for you get a fresh copy of the page.
  2. Navigate to a web page that you want to test (http://localhost:8080 in this case.)
  3. When the loading has finished, uncheck the “Disable cache” box.
  4. Don’t reload the page as this will cause the browser to contact the server to revalidate assets. Instead, navigate to the page. To do this on a page you’re already on, click in the address bar and hit enter.

When you do this, you can see the effects of your Cache-Control headers at work. Figure 6 shows a partial listing of assets in the network panel.


Figure 6 – The effects of our cache policy on the Weekly Timber website. The HTML is revalidated from the server on every request and the server returns a 304 status if the document hasn’t changed on the server. Items reading from the browser cache don’t trigger a return trip to the web server.

This caching strategy is optimal for our purposes. When the cache’s primed, there’s only one request made to validate the HTML file’s freshness with the server on a return visit. If the locally cached document’s still fresh, the total page weight is less than half a kilobyte. This makes subsequent visits to this page fast, and the assets shared on all pages are cached on subsequent pages, decreasing the load time of those pages as well.

Sometimes, when you update your website, you’ll need to invalidate assets in your browser cache. In the next section, I explain how to do exactly that.

Invalidating Cached Assets

You’ve been in a scenario like this: You’ve worked hard on a project for the folks at Weekly Timber some weeks, and finally deployed the site to production, only to find out hours later that there’s a bug. This bug can be in your CSS or JavaScript, or perhaps a content problem in an image or your HTML. The bugs have been fixed and deployed to production, but the problem’s that the site’s still not updating for your users because their browser cache is preventing them from seeing your changes.

Advice such as “reload the page” or “clear your cache” may pacify nervous marketers and business clients, but this isn’t how users normally interact with web pages. It won’t occur to users to reload a page in typical circumstances. This means you need to find a way to force the page’s assets to be downloaded again.

Though it is tedious, this is an easy problem to overcome. If you’re using the caching strategy outlined in the previous section, your browser will always validate the freshness of the HTML with the server. As long as this occurs, you’ve a good shot at getting updated assets out to users who’ve outdated ones in their browser cache.

Invalidating CSS and JavaScript Assets

Just your luck. the Weekly Timber website has a CSS or JavaScript bug in production that made it past QA somehow. With the bug identified and the fix deployed, you’re able to verify that it’s in production because you reloaded the page, but your project manager is insistent that users won’t see the updated content. Their concern’s warranted, and you need to do something.

The fix here’s rather simple. Remember that with our current caching policy in place, the browser always validates the HTML document’s freshness with the server. You can make a small change in the HTML that won’t only trigger it to download again, but will also trigger the modified assets to download again as well. All it requires is adding a query string to a CSS or JavaScript reference. If we need to force the CSS to update, we can update the tag reference to the CSS to something like this (change bolded):

?v=2" type="text/css">

Adding a query string to the asset causes the browser to download the asset again because the URL of the asset has changed. After this change in the HTML is uploaded to the server, site visitors with the old version of styles.min.css in their cache will now receive the new version. This same method can be used to invalidate any asset, including JavaScript and images.

This can come off as a hacky way of solving the problem. It’s a fine stopgap for when you need to make sure something won’t be cached, but you don’t want to have to be responsible for versioning your own files, either. A more convenient way around this is to use a server side language such as PHP to handle this problem for you automatically whenever you update a file. Listing 2 below shows one way of handling this problem:

Listing 2 Automated cache invalidation in PHP

 â ¶   " type="text/css"> â ·  

â ¶ Creates an MD5 hash of styles.min.css. This is unique based on the file’s contents,

â · Appends the hash string to the query string.

This solution works well because the file_md5 function generates an MD5 hash based on the contents of the file. If the file never changes, the hash stays the same. If even one byte of the file changes, the hash changes.

. You can accomplish this another way; use the language’s filemtime function to check for the file’s last modification time and use that instead. Or you can write your own versioning system. The point is this is illustrative of a concept: No matter what language you’re working with, there are tools there that can automate this for you.

Invalidating Images and Other Media Files

Sometimes the problem isn’t with your CSS or JavaScript, it’s with media files, such as images. You could use the query string method as explained earlier, but the more sensible thing might be to point to a new image file.

If you’re running a small website, such as in the case of the Weekly Timber website, it doesn’t make much of a difference if you use the query string trick or not. But if your site uses a content management system (CMS), this is the easiest way to deal with invalidating cached images. You may decide to migrate to a new CMS at any point – which is fine as it can help you to drive traffic and connect with potential customers – but just make sure you don’t make any of the common SEO mistakes associated with migrating to a new CMS. If you have a CMS, then upload a new image and the CMS will point to it. The new image URL, never having been previously cached by users before, will be reflected in the HTML and be immediately visible to your users.

Now that you’ve had a chance to work with Cache-Control through a practical example, explore what’s possible with caching on your own sites. See if there’s room for tweaks, and as always, test your changes and see how it affects your users.

Happy performance tweaking!

For more web site optimization tips and tricks, check out the book on liveBook here and this slide deck.