I have read several articles about browser caching. We mentioned also can always say a few key words, but when it comes to how to use may not be sure. So I write it down here through practical operation for better understanding and memorization.

First of all, there are two common cases of browser caches: strong cache and negotiated cache. Strong cache has higher priority, and negotiated cache is used only when a strong cache hit fails.

Strong cache

Strong cache does not need to send HTTP request, when the resource hit the strong cache, directly from the cache, the response status returns 200, open the console to check the Size of the resource does not show the Size, but tells us that the cache.

How is strong caching implemented, as demonstrated by Koa in this article’s back-end code

1, Expires

Set Expires after the first request. Set Expires in the response header

router.get('/img/1.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3))
    })
    ctx.body = file
})
Copy the code

Set the expiration time to three days later so that you do not need to re-request the server while the cache is still alive. The problem with Expires is that client time and server time can be inconsistent, such as when you set the computer time to three days or even a year, and the cache won’t work.

2, the cache-control

Cache-control takes precedence over Expires. When used together, cache-Control ignores Expires. Cache-control is flexible.

2.1 Max – age

Max-age Specifies the validity period of the resource, in seconds. Here’s Demo2, cache-control :max-age=0, and set Expires. Testing shows that images are never cached, which also shows priority issues.

// demo2
router.get('/img/2.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-Control': 'max-age=0'
    })
    ctx.body = file
})
Copy the code

Test Demo3 again so that the image cache time is 10 seconds

// demo3
router.get('/img/3.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Cache-Control': 'max-age=10'
    })
    ctx.body = file
})
Copy the code

2.2 s – maxage

S-maxage is similar to max-age, except that s-maxage is for proxy servers

// demo4
router.get('/img/4.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-Control': 's-maxage=10'
    })
    ctx.body = file
})
Copy the code

The test found that the image could not be cached

2.3 private and public

Related to the preceding two attributes, private indicates that resources can be cached by browsers, and public indicates that resources can be cached by both browsers and proxy servers. The default value is private, which is equivalent to max-age; When the s-maxage property is set, it indicates that it can only be cached by proxy servers.

Look at Demo5 here

// demo5
router.get('/img/5.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Cache-Control': 'private'
    })
    ctx.body = file
})
Copy the code

The test found that images are never cached, so the default value for private corresponding to max-age should be 0. But is private really the same as max-age=0?

//demo6
router.get('/img/6.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-Control': 'private'
    })
    ctx.body = file
})
Copy the code

The test found that the image was cached, indicating that the private case does not allow Expires.

2.4 no – store and no – cache

No-store is violent and does not apply to any caching mechanism. It directly sends requests to the server to download complete resources.

No-cache skips the strong cache, which means that Expires and max-age are invalid, and directly requests the server to check whether the resource has expired, which is the negotiation cache phase. There are two key fields in the negotiated cache, last-Modified and Etag

Negotiate the cache

1, last-modified and if-modified-since

Please see demo8

// demo8
router.get('/img/8.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    const stats = fs.statSync(path.resolve(__dirname,`.${ctx.request.path}`))
    if(ctx.request.header['if-modified-since'] === stats.mtime.toUTCString()){
        return ctx.status = 304
    }
    ctx.set({
        'Cache-Control': 'no-cache'.'Last-Modified': stats.mtime.toUTCString()
    })
    ctx.body = file
})
Copy the code

After the first request, a last-modified field is added to the response header to return the Last Modified time of the resource. The next request header is added with the if-Modified-since field, which is the value of the last-modified value in the previous response header. The status code of 304 is returned.

Cache-control: If the ‘no-cache’ is removed, the cache will be strong, and the if-modified-since in the request header will not be detected.

ETag and if-none-match

Please see demo9

// demo9
router.get('/img/9.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    const stats = fs.statSync(path.resolve(__dirname,`.${ctx.request.path}`))
    if(ctx.request.header['if-none-match']) {if(ctx.request.header['if-none-match'= = ='abc123') {return ctx.status = 304}}else if(ctx.request.header['if-modified-since'] === stats.mtime.toUTCString()){
        return ctx.status = 304
    }
    ctx.set({
        'Cache-Control': 'no-cache'.'Last-Modified': stats.mtime.toUTCString(),
        'ETag': 'abc123'
    })
    ctx.body = file
})
Copy the code

After the first request, an ETag field is added to the response header. ETag generates an identifier from the content of the resource. In the next request, an if-none-match field is added to the request, and the value is the ETag value in the previous response header. ETag can be said to be a complement to Last-Modified, because last-Modified also has shortcomings. For example, the time in last-Modified is accurate to the second, and if a file is Modified once within the same second, the next request is expected to fetch a new resource but will actually go through the negotiation cache. ETag, on the other hand, is based on the resource content, so new values are generated, so it works as expected.

Added: Four locations for the browser cache

  1. Service Worker Cache
  2. Memory Cache
  3. Disk Cache
  4. Push Cache

Service Worker Cache is an important implementation mechanism for PWA applications.

Memory Cache and Disk Cache are summarized above. We strongly Cache and negotiate the location of Cache resources. Memory Cache is the most efficient, of course, Memory resources are expensive and limited, not all Memory Cache, Disk Cache, relatively slow read. In general, large files or high memory usage will result in resources being thrown to disk.

Push Cache is the last line of defense for caching. Is the content of HTTP2, need to know for yourself.

To sum up a few points

1. There are two sets of agreed fields in the negotiated cache: last-modified and if-Modified-since; The second is ETag and if-none-match. If a last-Modified (or ETag) field exists in the response header, the next request header will automatically add the if-Modified-since (or if-none-match) field. And because the second set was more accurate, it was given higher priority.

Cache-control: private and cache-Control: max-age=0 have different effects. The former does not make Expires invalid.

3, browser cache mechanism overview, first if hit strong cache directly use; Otherwise, the negotiation cache phase is entered, where HTTP requests are made to check whether the resource is updated by negotiating two sets of cached specifications. If there is no update, 304 status code is returned, otherwise the resource is retrieved and 200 status code is returned.

4. As can be seen from the above demonstration, the workload of handling cache is mainly in the back end, and in the work, we usually use the middleware to deal with static resources directly. For example, in the author’s Koa project, we will use the MIDDLEWARE of KOA-static. There is no front-end code to write, and the back-end uses off-the-shelf wheels, so caching knowledge is discarded.

5, after watching these demos, I believe you will be able to remember the demo address, you can also clone down to run several times to try, I hope this article is helpful to you.