Skip to main content

How to improve your page load time with browser caching

· Updated on · 16 min read
Umar Hansa

Caching is the process of storing copies of files in a cache, or temporary storage location, so that they can be accessed more quickly. When a browser caches a file, it stores a copy of that file on the user's device. This means that the next time the user visits the same page, the browser can load the file from the cache instead of downloading it again from the server.

How browser caching helps with page load

The most performant request is the one not made.

When a user visits a website, their browser needs to download all the resources needed to display the page. This includes HTML, CSS, JavaScript, images, and other assets. If the browser already has a copy of a resource in its cache, it can load that resource much faster than if it had to download it from the server.

Here's a simple diagram that shows how a page is requested from a server. In this example, the user waits for over a second for the page to load.

Without caching:

And here's a diagram that shows how a page is retrieved from the cache, without contacting the server. In this example, the user gets the page instantly:

With caching:

In these examples, the cache, just like the browser, lives on the user's device. That's why it's so fast.

To give a practical example, here are two test runs showing the loading of an Amazon product page without caching, and with caching:

Amazon product page without vs. with caching

While there are many interesting metrics to look at, let's focus on the Page weight metric:

  • Without caching: The Page weight is 4.34 MB.
  • With caching: The Page weight is 1.63 MB.

This is a 62% reduction in page weight, which is a significant improvement in page load time.

tip

For a resource to be cached on the users device, the user must have visited the page before. Therefore caching is most effective for subsequent visits to a page.

How browser caching helps with Core Web Vitals

Core Web Vitals are a set of metrics that measure the user experience of a website. They are measured by Google, and can affect your website's search ranking.

Core Web Vitals are not just measured for the first visit to a website, but also for repeat visits. This is where browser caching can help.

When your user gets cached resources, those resources load faster, which can improve First Contentful Paint (FCP), and Largest Contentful Paint (LCP) metrics, among others.

This screenshot shows the improvements gained from caching on the Apple home page:

Apple home page Core Web Vitals without vs. with caching

Interestingly, while the LCP score improved, the FCP worsened. This highlights the importance of testing your website after implementing caching. But generally, variability is to be expected in web performance testing.

Types of caches

There are different types of caches that can be used to store copies of files:

  • Browser cache: Stored on the user's device, in the browser. Note a browser cache can include a Service Worker cache, back forward cache, and Cache Storage API.
  • CDN cache: Stored on a content delivery network (CDN), which is a network of servers that are distributed around the world.
  • Proxy cache: Stored on a proxy server, which is a server that sits between the user and the web server.
  • Application cache: Stored in the application itself, and can be used to store data that is needed by the application.

Earlier, you saw how a resource cached on a local device can be served instantly. But then how does that work for a CDN, if it's cached on the CDN, does the user get it instantly?

The answer is no. Different caches have different speeds and performance characteristics. A cache on the user's device is the fastest. On average, a CDN cache is faster than going to the origin server.

tip

A browser cache is known as a private cache, whereas a CDN cache is known as a public cache.

This post primarily focuses on browser caching, note however that for browser caching to work effectively, the server must also be configured to send the correct caching headers.

The Cache-Control header

One of the most important headers for controlling caching is the Cache-Control header. This header deals with how a resource should be cached, and how it should be revalidated.

tip

You should always be explicit and intentional about caching. Use a Cache-Control header, otherwise heuristic caching will be used, which can lead to unpredictable results.

A cache control header can have one or more directives. For example: Cache-Control: no-store means the browser won't cache the resource at all. Here are some other directives:

  • no-store: The browser won't cache the resource at all.
  • private: The resource can only be cached by the browser cache.
  • max-age: The time is seconds that the resource is considered fresh.
  • public: The resource can be cached by any cache, including the browser cache.
  • immutable: The resource is considered immutable, meaning it will never change.
  • no-cache: The browser will still cache the resource, but will revalidate it with the server.
  • must-revalidate: The browser must revalidate a stale resource with the server before using it.
  • s-maxage: The time in seconds that the resource is considered fresh for shared caches like a CDN.
  • stale-while-revalidate: The browser can serve a stale version of the resource while it revalidates it with the server.

You don't need to memorize all of these directives, but it's good to know they exist.

When you figure out an appropriate cache policy for your resources, you'll set the Cache-Control header on your server. Most web frameworks and web servers have built-in support for setting headers and directives.

Cache-control examples you can get started with

For dynamic resources, like HTML pages (for example, https://example.com/shop), start out with:

max-age=0,must-revalidate,public

Explanation: the browser will always revalidate the resource with the server, and the resource is considered fresh for 0 seconds. This is a good starting point for dynamic resources like HTML pages.

info

Take note that using max-age=0,must-revalidate,public does allow the browser to cache the resource, but it must revalidate it with the server before using it. In most cases, this is a safe approach to take, however there's a reason they say that cache invalidation is one of the hardest problems in computer science and it's not a coincidence that many large organisations request that users manually clear their cache when their users encounter issues.

For static versioned assets (for example, https://example.com/main.12345678.css), like images, CSS, and JavaScript, start out with:

public, max-age=31536000, immutable

Explanation: the browser will cache the resource for 1 year, and the resource is considered immutable, meaning it will never change.

info

If your assets are not versioned (e.g. they live at https://example.com/main.css instead of https://example.com/main.12345678.css), you should work towards versioning them before using this cache policy.

Cache revalidation

Cache revalidation is the process of checking with the server if a cached resource is still valid.

If you cache resources on your website, you must ensure you've tested your cache invalidation or revalidation strategy.

Let's say there's a cached resource that is 20 seconds old. As the resource was originally served with max-age=10,must-revalidate,public, it's now considered stale. Should the browser now remove that resource from the cache? Not necessarily.

The browser can revalidate the resource with the server to check if it's still valid. If the resource hasn't changed on the server, the server can respond with a 304 Not Modified response, which is much faster than sending the entire resource again.

This screenshot shows a fully cached resource that is fresh. The browser didn't need to consult the server at all:

Fully cached resource

This screenshot shows a cached resource that is stale. The browser needs to revalidate the resource with the server:

Stale cached resource

The small amount of data exchanged with the server is for revalidation. But note that the response body (the content of the resource) is empty. This is because the resource hasn't changed, so the server doesn't need to send the resource again.

tip

How does revalidation work? The client can send an ETag to the server to check if the resource has changed. You can think of an ETag as a unique identifier for a resource. If the resource changes even a little bit, the ETag will change.

Here's a more concrete example that reiterates what the previous screenshots show:

First page load

  • Response header: Cache-Control: public, max-age=10
  • Response body: 1mb of data

Second page load (within 10 seconds)

The browser doesn't make a request to the server, this is highly efficient:

  • Request headers: None
  • Response headers: None
  • Response body: None

Third page load (after 10 seconds)

The browser makes a request to the server. This continues to be efficient, however it does involve a network request to the server:

  • Request headers: If-Modified-Since: 01 Jan 2025 18:30:00 GMT and If-None-Match: W/"164-198173818fc"
  • Response headers: 304 Not Modified
  • Response body: 0 bytes

Learn more about conditional requests here.

tip

When constructing network requests with the fetch API, you can use the cache option to send the appropriate cache headers:

fetch("/", { cache: "no-cache" });

Cache busting

Cache busting is a technique used to force the browser to download a new version of a file, even if the file hasn't changed.

If you use caching headers like max-age=31536000 for a CSS file, the browser will cache that file for a year. If you update the CSS file, the browser won't know about the update until the cache expires. This is where cache busting comes in.

While the exact technique will vary depending on your setup, we recommend using a versioned file name. For example, instead of main.css, you could use main.12345678.css. The version number is typically a hash of the file contents, so if the file changes, the hash will change, and the browser will download the new file.

Unless you have a highly bespoke setup, your existing tooling should be able to handle cache busting for you. To find out how to do this, search for "cache busting" / "asset versioning" in your framework or build tool documentation.

Import maps

Import maps work nicely with cache busting. They allow you to define a mapping between a module specifier and a URL, and this can be used to refer to versioned files in one place.

Your other modules can then import the module specifier, and the browser will automatically fetch the correct versioned file:

HTML (index.html):

<script type="importmap">
{
"imports": {
"utils": "https://cdn.com/utils.12345678.js"
}
}
</script>

JavaScript (main.123.js):

// No need for import * as utils from "https://cdn.com/utils.12345678.js";
import * as utils from "utils";

console.log(utils);

This approach works well, and mitigates the invalidation chain problem that would occur if the contents of main.123.js were updated to refer to a new version of utils. As in such a scenario, the browser would need to download both files again. Whereas with import maps, the browser will only download the updated file (utils).

Serve static assets with an efficient cache policy

Serve static assets with an efficient cache policy is a Lighthouse audit that validates you are caching resources that are considered reasonable to cache.

Serve static assets with an efficient cache policy

You can pass this audit by using the cache-control header with a suitable max-age directive, such as the ones discussed earlier.

DevTools and caching

All modern browser developer tools expose some caching information. This section focuses on Chrome DevTools.

  1. Within Chrome DevTools (and there will be a similar setting for other browsers developer tools), ensure "Disable cache" is unchecked.
    • This will ensure that the browser cache is used as it would be in a real-world scenario.
  2. Then, navigate to a page, and inspect the network panel.
  3. Reload the page - just click the reload icon to avoid doing a hard reload which may bypass the cache.
  4. Observe the network panel again. The "Size" column should show disk cache or similar for cached resources.

Disable cache in Chrome DevTools

Clicking a network resource in the network panel shows you HTTP request and response headers, however for convenience, you configure DevTools to always show cache-related headers in the overall network panel:

  1. Right click on the network panel columns, and navigate to "Response headers".
  2. Check the options for "etag", "cache-control", and "last-modified".

DevTools cache headers

DevTools now shows you those network response headers for each request, without needing to click into each one.

Caching on a web server

Caching is a big topic, and implementation details are out of scope for this post. However, here are some resources to get you started.

info

These examples are simplified for the sake of brevity. You should always consult the official documentation for your web server.

These examples illustrate the basics of caching:

  • For dynamic resources like HTML pages, always revalidate.
  • For static versioned assets like images, CSS, and JavaScript, cache for a long time.

Caching with the Caddy web server:

# Dynamic content (HTML pages)
@root {
path /
not path /static/*
}
header @root Cache-Control "max-age=0, must-revalidate, public"

# Static versioned assets
@static {
path /static/*
}
header @static Cache-Control "public, max-age=31536000, immutable"

Learn more about Caddy's caching directives here.

Caching with the Nginx web server:

location / {
add_header Cache-Control "max-age=0, must-revalidate, public";
}

location /static/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

Learn more about Nginx's content caching.

Caching with the Apache web server:

# Dynamic content (HTML pages)
<Location "/">
Header set Cache-Control "max-age=0, must-revalidate, public"
</Location>

# Static versioned assets
<Location "/static/">
Header set Cache-Control "public, max-age=31536000, immutable"
</Location>

Learn more about Apache's mod_headers.

Caching on CDNs

Caching on CDNs is a big topic, and implementation details are out of scope for this post. However, here are some resources to get you started.

tip

Using CDNs can often carry their own edge cases. You may need to supply special cache-control directives to the CDN, or use a special API to purge the cache.

Be sure to consult the official documentation for your CDN.

Caching in web frameworks

Just like with CDNs, caching with web frameworks is a big topic, and implementation details are out of scope for this post. However, here are some resources to get you started.

Increase the chances of a cache hit

With browser caching, a cache hit is when the browser can serve a resource from the cache, without needing to make a network request. A cache miss is when the browser needs to make a network request to get the resource.

  • Use effective cache policies: Use the Cache-Control header to control how resources are cached, and be intentional about how long resources are cached for. The longer a resource can be considered fresh, the more likely it is to be in the cache.
  • Don't invalidate cached resources: If a resource hasn't changed, don't change the URL. If you end up bundling all your frontend assets into a single file, and one of those assets changes, the entire bundle will need to be re-downloaded.
  • Don't forget the cache hit ratio on the CDN: If you're using a CDN, you'll want to read the CDN's documentation to understand how to increase the cache hit ratio in ways that are specific to that CDN.

How to clear a browser cache

Clear your own browser cache in Chrome:

  1. Click the three dots in the top right corner.
  2. Click Delete browsing data.
  3. Select the options you want to clear, and click Delete.

Clear a cache for your users:

You can force a clearing of the cache and other site data with the Clear-Site-Data response header:

Clear-Site-Data: cache

This header can be used to clear the cache, cookies, storage, and other site data.

How to see caching info in DebugBear

DebugBear surfaces caching information in both Real User Monitoring (RUM) and synthetic tests.

In RUM:

The page weight chart in the Page Views tab shows the cache ratio for each page view.

RUM cache ratio chart

Also in the Page Views tab, you can click on a specific page view to see a network request waterfall. Cached resources are marked with a Cache badge.

RUM cache badge

Using RUM to identify cached resources gives you a definitive answer on how well your caching strategy is working for real users.

In synthetic (lab) tests:

You can warm the cache in the test run settings, and then look at the request waterfall to see which resources were cached.

Warm cache settings

tip

Don't underestimate the value of RUM data. Lab based tests run in a controlled and repeatable environment, but RUM captures the real-world experience of your users. This can be especially important for caching, as there are all sorts of reasons as to why caching may not work as expected:

  • The user's browser may have a full cache, or limited disk space.
  • The user may have disabled caching in their browser settings.
  • The user may have a browser extension that interferes with caching.
  • The user may be using incognito mode, which disables caching.
  • The user may be connected to a network that modifies caching headers.

Conclusion

When it comes to caching, the key is to be intentional. Use the Cache-Control header to control how resources are cached, and how they are revalidated. Cache static assets for a long time, and dynamic resources for a short time, or not at all.

Get a monthly email with page speed tips