The subject of Web resource caching is as old as the World Wide Web itself. However, I'd like to offer an as-exhaustive-as-possible catalog of how one can improve performance by caching. Web resource caching can happen in two different places: client-side - on the browser and server-side. This post is dedicated to the former; the next post will focus on the latter. Caching 101 The idea behind caching is simple: if a resource is a time- or resource-consuming to compute, do it once and store the result. When somebody requests the resource afterward, return the stored result instead of computing it a second time. It looks simple - and it is, but the devil is in the detail, as they say. The problem is that a "computation" is not a mathematical one. In mathematics, the result of a computation is constant over time. On the Web, the resource you requested yesterday may be different if you request it today. Think about the weather forecast, for example. It all boils down to two related concepts: and . freshness staleness A fresh response is one whose age has not yet exceeded its freshness lifetime. Conversely, a stale response is one where it has. A response's freshness lifetime is the length of time between its generation by the origin server and its expiration time. An explicit expiration time is the time at which the origin server intends that a stored response can no longer be used by a without further validation, whereas a heuristic expiration time is assigned by a when no explicit expiration time is available. A response's age is the time that has passed since it was generated by, or successfully validated with, the origin server. Cache Cache When a response is "fresh" in the cache, it can be used to satisfy subsequent requests without contacting the origin server, thereby improving efficiency. -- RFC 7234 - 4.2. Freshness Early Web resource caching Remember that the WWW was relatively simple at its beginning compared to nowadays. The client would send a request, and the server would return the requested resource. When the resource was a page, whether it was a static page or a server-rendered page was unimportant. Hence, early client-side caching was pretty "rustic". The first specification of Web caching is defined in , HTTP/1.1 Caching, in 2014. Note that it has been superseded by since 2022. RFC 7234 aka RFC 9111 I won't talk here about the HTTP header since it's deprecated. The most straightforward cache management is through the response header. When the server returns the resource, it specifies after which timestamp the cache is stale. The browser has two options when a cached resource is requested: Pragma Expire Either the current time is the expiry timestamp: the resource is considered fresh, and the browser serves it from the local cache before Or it's : the resource is considered stale, and the browser requires the resource from the server as it was not cached after The benefit of is that it's a purely local decision. It doesn't need to send a request to the server. However, it has two main issues: Expire The decision to use the locally cached resource (or not) is based on heuristics. The resource may have changed server-side despite the value being in the future, so the browser serves an out-of-date resource. Conversely, the browser may send a request because the time has expired, but the resource hasn't changed. Expiry Moreover, is pretty basic. A resource is either fresh or stale; either return it from the or send the request again. We may want to have more control. Expire Cache Cache-Control to the rescue The header aims to address the following requirements: Cache-Control Never cache a resource at all Validate if a resource should be served from the cache before serving it Can intermediate caches (proxies) cache the resource? is an HTTP header used on the request the response. The header can contain different directives separated by commas. Exact directives vary depending on whether they're part of the request or the response. Cache-Control and All in all, is quite complex. It might be well the subject of a dedicated post; I won't paraphrase the . Cache-Control specification However, here's a visual help on how to configure response headers. Cache-Control The page of Mozilla Developer Network has some significant use cases of , complete with configuration. Cache Control Cache-Control As , is also : the browser serves the resource from its cache, if needed, without any request to the server. Expire Cache-Control local Last-Modified and ETag To avoid the risk of serving an out-of-date resource, the browser send a request to the server. Enters the response header. works in conjunction with the header: must Last-Modified Last-Modified If-Modified-Since request The request HTTP header makes the request conditional: the server sends back the requested resource, with a status, only if it has been last modified after the given date. If the resource has not been modified since, the response is a without any body; the response header of a previous request contains the date of last modification. Unlike , can only be used with a or . If-Modified-Since 200 304 Last-Modified If-Unmodified-Since If-Modified-Since GET HEAD -- If-Modified-Since Let's use a diagram to make clear how they interact: Note: the has the opposite function for and other non-idempotent methods. It returns a HTTP error to avoid overwriting resources that have changed. If-Unmodified-Since POST 412 Precondition Failed The problem with timestamps in distributed systems is that it's impossible to guarantee that all clocks in the system have the same time. Clocks drift at different paces and need to to the same time at regular intervals. Hence, if the server that generated the header and the one that receives the header are different, the results could be unexpected depending on their drift. Note that it also applies to the header. synchronize Last-Modified If-Modified-Since Expire Etags are an alternative to timestamps to avoid the above issue. The server computes the hash of the served resource and sends the header containing the value along with the resource. When a new request comes in with the containing the hash value, the server compares it with the current hash. If they match, it returns a as above. ETag If-None-Match 304 It has the slight overhead of computing the hash vs. just handing the timestamp, but it's nowadays considered a good practice. The Cache API The most recent way to cache on the client side is via the . It offers a general cache interface: you can think of it as a local key-value provided by the browser. Cache API Here are the provided methods: Method Description Cache.match(request, options) Returns a that resolves to the response associated with the first matching request in the object. Promise Cache Cache.matchAll(request, options) Returns a that resolves to an array of all matching responses in the object. Promise Cache Cache.add(request) Takes a URL, retrieves it and adds the resulting response object to the given cache. This is functionally equivalent to calling , then using to add the results to the cache. fetch() put() Cache.addAll(requests) Takes an array of URLs, retrieves them, and adds the resulting response objects to the given cache. Cache.put(request, response) Takes both a request and its response and adds it to the given cache. Cache.delete(request, options) Finds the entry whose key is the request, returning a that resolves to if a matching entry is found and deleted. If no entry is found, the resolves to . Cache Promise true Cache Cache Promise false Cache.keys(request, options) Returns a that resolves to an array of keys. Promise Cache The Cache API works in conjunction with . The flow is simple: Service Workers You register a service worker on a URL The browser calls the worker before the URL fetch call From the worker, you can return resources from the cache and avoid request to the server any It allows us to put resources in the cache after the initial load so that the client can work offline - depending on the use case. Summary Here's a summary of the above alternatives to cache resources client-side. Order Alternative Managed by Local Pros Cons 1 Service worker + Cache API You Yes Flexible - Requires JavaScript coding skills - Coding and maintenance time 2 Expire Browser Yes Easy configuration - Guess-based - Simplistic 2 Cache-Control Browser Yes Fine-grained control - Guess-based - Complex configuration 3 Last-Modified Browser No Just works Sensible to clock drift 3 ETag Browser No Just works Slightly more resource-sensitive to compute the hash Note that those alternatives aren't exclusive. You may have a short header and rely on . You should probably use both a level 2 alternative and a level 3. Expire ETag A bit of practice Let's put the theory that we have seen above into practice. I'll set up a two-tiered HTTP cache: The first tier caches resources locally for 10 seconds using Cache-Control The second tier uses to avoid optimizing the data load over the network ETag I'll use . APISIX sits on the shoulder of giants, namely NGINX. NGINX adds response headers . Apache APISIX ETag by default We only need to add the response header. We achieve it with the plugin: Cache-Control response-rewrite upstreams: - id: 1 type: roundrobin nodes: "content:8080": 1 routes: - uri: /* upstream_id: 1 plugins: response-rewrite: headers: set: Cache-Control: "max-age=10" Let's do it first. without a browser curl -v localhost:9080 HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 147 Connection: keep-alive Date: Thu, 24 Nov 2022 08:21:36 GMT Accept-Ranges: bytes Last-Modified: Wed, 23 Nov 2022 13:58:55 GMT ETag: "637e271f-93" Server: APISIX/3.0.0 Cache-Control: max-age=10 To prevent the server from sending the same resource, we can use the value in an request header: ETag If-None-Match curl -H 'If-None-Match: "637e271f-93"' -v localhost:9080 The result is a as expected: 304 Not Modified HTTP/1.1 304 Not Modified Content-Type: text/html; charset=utf-8 Content-Length: 147 Connection: keep-alive Date: Thu, 24 Nov 2022 08:26:17 GMT Accept-Ranges: bytes Last-Modified: Wed, 23 Nov 2022 13:58:55 GMT ETag: "637e271f-93" Server: APISIX/3.0.0 Cache-Control: max-age=10 Now, we can do the same inside a browser. If we use the feature a second time before 10 seconds have passed, the browser returns the resource from the cache without sending the request to the server. resend Conclusion In this post, I described several alternatives to cache web resources: and , and , and the Cache API and web workers. Expiry Cache-Control Last-Modified ETag You can easily set the HTTP response headers via a reverse proxy or an API Gateway. With Apache APISIX, ETags are enabled by default, and other headers are easily set up. In the next post, I will describe caching server-side. You can find the source code for this post on . GitHub To go further: RFC 7234: HTTP/1.1: Caching (obsolete) RFC 9111: HTTP Caching HTTP caching Cache-Control Prevent unnecessary network requests with the HTTP Cache Cache API Service worker caching and HTTP caching Originally published at on November 27th, 2022 A Java Geek