By reusing previously acquired resources, you can significantly improve the performance of your website and applications. Web caching reduces wait times and network traffic. By using HTTP caching, it becomes more responsive.

Typically http caching is divided into strong caching and negotiated caching.

1. Strong caching

Use browser local cache directly within the agreed fixed time. This approach is suitable for resources that are not updated frequently, such as js,css,image and other static resources; also this is a disadvantage, if the cache time is set too long, the updated content is not timely; if the cache time is set too short, it causes the problem that the content is not updated, but the cache time is up.

There are two general ways to set strong caching.

  1. setting the expiration point of the current resource through Expires.
  2. setting the cache time from the first request through cache-control.

1.1 Expires

Set this field on the server side to indicate the expiration time of this resource.

1
Expires: Wed, 21 Oct 2017 07:28:00 GMT

The browser then takes its local time and compares it to the time in this field, and if it has not yet reached its expiration time, it continues to use the cache, otherwise it generates a new request.

This presents a problem in that the length of that resource cache is related to its local time, e.g. setting the local computer time in 2039 for all resources currently being sent down, cannot be cached.

1.2 cache-control

Expires is a field that existed since HTTP 1.0. To solve the above problem with Expires, the cache-control field was introduced in HTTP 1.1. Instead of using a uniform expiration time, this field tells the browser how long to cache the request, starting from the first time it is received.

If we build a simple http service with nodejs, setting this field is also simple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const http = require('http');

http
  .createServer((req, res) => {
    console.log('get static request:', req.url);

    res.writeHead(200, {
      'cache-control': 'public, max-age=31536000',
    });
    res.end('console.log(Date.now())');
  })
  .listen(3031);

If you use express, you can set it up like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const express = require('express');

const app = express();
app.get('*', (req, res) => {
  console.log('get static request:', req.url);

  // 下面的3种方式都可以
  res.setHeader('cache-control', 'public, max-age=600');
  // res.header('cache-control', 'public, max-age=600');
  // res.set({ 'cache-control': 'public, max-age=600' });
  res.end('console.log(Date.now())');
});
app.listen(3031);

When the cache works, the resource is prompted from memory cache or from disk cache and the server no longer receives the request.

In Chrome, when refreshing a page, you will see that cache-control or Expires is disabled. This is because Google Chrome ignores the header Cache-Control or Expires header if the request is made immediately after another request for the same URI in the same tab (by clicking the refresh button, or F5, or something like that). It may have an algorithm to guess what the user really wants to do. One way to test the Cache-Control header is to return an HTML document with a link to itself. When that link is clicked, Chrome drops the document from its cache.

So how can I see the effect? Open a new window, open the console, then request the link again and you’ll see that it’s gone cached, Is Chrome ignoring Cache-Control: max-age?.

1.3 Other uses of cache-control

1.3.1 Components of cache-control

The value of cache-control consists of 3 main parts, any of which can be used individually or in combination (see the document Cache-Control).

  • Cacheability: i.e., how the resource is cached

    • public: indicates that the response can be cached by any object (including: the client sending the request, the proxy server, etc.), even if the content is not normally cacheable. (For example: 1. the response does not have a max-age directive or Expires message header; 2. the response corresponds to a request method that is POST.) private: indicates that the response can only be cached.
    • private: indicates that the response can only be cached by a single user, not as a shared cache (i.e., the proxy server cannot cache it). A private cache can cache the content of the response, e.g., the local browser of the corresponding user.
    • no-cache: forces the cache to submit the request to the original server for validation (negotiated cache validation) before publishing a cached copy.
    • no-store: The cache should not store anything about the client request or the server response, i.e., no cache is used. This no-store is stricter than no-cache, where no-cache is a no-go strong cache, but negotiated cache can still be used, but no-store is a no-cache of any content.
  • Expiration time

    • max-page=<seconds>: Sets the maximum period of cache storage beyond which the cache is considered expired (in seconds). In contrast to Expires, the time is relative to the time of the request.
    • s-maxage=<seconds>: overrides the max-age or Expires header, but only for shared caches (such as individual proxies); private caches will ignore it.
  • Revalidate and reload

    • must-revalidate: Once a resource has expired (e.g. has exceeded max-age), the cache cannot respond to subsequent requests with that resource until it has successfully validated with the original server.
    • proxy-revalidate: Works the same as must-revalidate, but it only applies to shared caches (such as proxies) and is ignored by private caches.
    • immutable Experimental: Indicates that the response body does not change over time. The resource (if not expired) does not change on the server, so the client should not send a revalidation request header (such as If-None-Match or If-Modified-Since) to check for updates, even if the user explicitly refreshes the page. In Firefox, immutable can only be used for https:// transactions.

1.3.2 Example of use

These values can be used in any combination, e.g.

Files that are generally not too altered can be cached in all segments for 600s (↓).

1
cache-control: public, max-age=600;

Close the cache, no storage anywhere (↓).

1
cache-control: no-store;

Clients can cache resources, but must re-validate them each time (↓), e.g. by specifying no-cache or max-age=0 , must-revalidate, etc.

1
Cache-Control: no-cache;
1
Cache-Control: max-age=0, must-revalidate;

1.4 Priority of cache-control and Expires

Both of these fields determine the expiration time of a resource, so if both are present, which one will the browser choose?

As defined in RFC2616.

If a response includes both an Expires header and a max-age directive, the max-age directive overrides the Expires header, even if the Expires header is more restrictive. This rule allows an origin server to provide, for a given response, a longer expiration time to an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be useful if certain HTTP/1.0 caches improperly calculate ages or expiration times, perhaps due to desynchronized clocks.

When both are present, max-age has higher priority.

1.5 Difference between memory cache and disk cache

Memory cache is the cache in memory, which mainly contains the resources that have been crawled in the current page, such as styles, scripts, images, etc. that have been downloaded from the page. Although the memory cache is efficient for reading, the cache is short-lived and will be released with the release of the process. Once we close the Tab page, the in-memory cache is also released.

So since the in-memory cache is so efficient, can’t we just keep all the data in memory?

This is not possible. The memory in a computer must be much smaller than the hard disk, and the operating system needs to be careful about how much memory it uses, so we must not have much memory to use.

When we refresh the page again after visiting it, we can find that much of the data comes from the memory cache.

An important cached resource in the memory cache is the resource downloaded by the preloader directive (for example). The preloader directive is known to be a common means of page optimization, parsing js/css files while the next resource is requested on the network.

One thing to note is that memory cache does not care about the value of the HTTP cache header Cache-Control of the returned resource when caching it, and the matching of resources is not just for URLs, but may also be for other features such as Content-Type, CORS, etc..

Disk Cache is also a cache stored in the hard disk, the read speed is slower, but everything can be stored to disk, compared to Memory Cache wins in capacity and storage timeliness.

Of all browser caches, Disk Cache has basically the largest coverage. It determines which resources need to be cached based on fields in HTTP Herder, which resources can be used directly without requesting them, and which resources have expired and need to be re-requested. And even in the case of cross-site, once a resource at the same address is cached by the drive, it will not be requested again. The vast majority of caching comes from Disk Cache, which is described in more detail below in the protocol header field for HTTP.

What files does the browser throw into memory? What goes to the hard drive?

There are different opinions on this, but the following are more reliable.

  • For large files, the probability is that they are not stored in memory, and the reverse takes precedence.
  • files are stored into the hard disk first if the current system memory usage is high.

The process of requesting data under strong cache.

process of requesting data

2. Negotiating the cache

Based on the content’s last-modified time (Last-Modified), or identifier (ETag), to determine if the content has changed, and if not, tell the browser to just use the cache, otherwise return the latest content.

Negotiating the cache is done each time the server is requested, and then the server verifies whether to go to the cache, or to send down new content. When the client-side cache can be used, it simply returns the 304 status code.

2.1 last-modified

last-modified is a response header that indicates when a resource was last modified and is used to determine if the received or stored resources are consistent with each other.

2.1.1 Using last-modified to implement caching

If the last response has a last-modified field, then later requests for resources will automatically have an If-Modified-Since field in the header with the value returned by the last-modified. Our server can compare this If-Modified-Since field with the modification time of the resource, and if there is no change, it will return 304 directly, and if there is a change, it will send the new content and the new last-modified field.

Let’s take express framework as an example to implement this caching logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const express = require('express');
const fs = require('fs');

const app = express();
let lastModifiedTime = 0;
app.get('*', (req, res) => {
  console.log('get static request:', req.url);

  // 获取请求头中的 if-modified-since ,若有该字段,且能与上次的修改时间相同
  // 则直接返回304
  const ifModifiedSince = req.headers['if-modified-since'];
  if (ifModifiedSince && new Date(ifModifiedSince).valueOf() === lastModifiedTime) {
    res.status(304).end();
    return;
  }

  const file = './app/static/hunger.js';
  // 读取文件时,实际上应当使用 createReadStream ,我们使用readFile仅是为了方便演示,
  // 因为使用readFile在高并发时,会产生内存积压的问题
  // readFile会将所有的内容都读取到内存中,
  // 而 createReadStream 则是分片读取,只要读取了内容就会传向下一个管道
  fs.readFile(file, { encoding: 'utf8' }, (err, data) => {
    if (err) {
      return res.status(500).end('read file error');
    }
    const { mtimeMs } = fs.statSync(file);
    // 存储该文件最后的修改时间,并将其设置为响应头进行返回
    lastModifiedTime = Math.floor(mtimeMs);
    res.setHeader('last-modified', new Date(lastModifiedTime).toGMTString());
    res.end(data);
  });
});
app.listen(3031);

This allows you to determine whether to use caching by the modification time of a resource.

2.1.2 Defects of last-modified

However, this field is not very accurate when determining consistency for the following reasons.

  1. last-modified is only accurate to the second level at most, so if there are multiple modifications within 1 second, it will be considered as no changes.
  2. if there are duplicate uploads of the file, or if the document is opened and then saved, this may result in a modification of the time, but the content does not actually change.

In response to these problems, another etag response field has appeared in the http protocol.

2.2 etag

An etag is an identifier, such as hash value, for the content of a resource. This makes caching more efficient and saves bandwidth, since the web server does not need to send a full response if the content has not changed. And if the content changes, using an ETag helps prevent simultaneous updates to resources from overwriting each other (“collisions in the air”).

If the content of a resource in a given URL changes, a new Etag value must be generated. Etags are therefore similar to fingerprints and may also be used by some servers for tracking purposes. Comparing etags can quickly determine if this resource has changed, but it may also be permanently retained by the tracking server.

2.2.1 Caching with etags

If the last response had an etag field, then subsequent requests for resources will automatically have an If-None-Match field in the header with the value returned by the last etag. Our server can compare this If-None-Match field with the tag of the resource, and if there is no change, it will return 304 directly, and if there is a change, it will send the new content and the new etag field.

Let’s take express framework as an example to implement this caching logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const express = require('express');
const fs = require('fs');
const crypto = require('crypto');

const md5 = (str) => crypto.createHash('md5').update(str).digest('hex');

const app = express();
let etag = '';
app.get('*', (req, res) => {
  console.log('get static request:', req.url);

  // 获取请求头中的 if-none-match ,若有该字段,且能与上次的etag相同
  // 则直接返回304
  const ifNoneMatch = req.headers['if-none-match'];
  if (ifNoneMatch && ifNoneMatch === etag) {
    res.status(304).end();
    return;
  }

  const file = './app/static/hunger.js';
  // 同理这里实际上我们最好使用 createReadStream
  fs.readFile(file, { encoding: 'utf8' }, (err, data) => {
    if (err) {
      return res.status(500).end('read file error');
    }
    // 这里我们使用md5来生成etag标识
    etag = md5(data);
    res.setHeader('etag', etag);
    res.end(data);
  });
});
app.listen(3031);

2.2.2 etag and last-modified

When both etag and last-modified are present, etag has higher priority; similarly, if-none-match has higher priority than if-modified-since.

Also, etag has better granularity of control over resource updates than last-modified, so no matter how the source file is modified, as long as the content remains the same, etag remains the same and the cache can be used forever.

The request flow under negotiated caching.

request flow under negotiated caching

3. Summary

We have explained the mechanism and implementation of mandatory caching and negotiated caching, and you can decide which caching method to use and how long to cache based on the caching characteristics of your resources.

In general, mandatory caching has a higher priority than negotiated caching, giving priority to determining whether mandatory caching exists.

http caching mechanism