preface

Browser caching is a very important solution for performance optimization. Using caching properly can improve user experience and save server overhead. Mastering the principles of caching and using it properly is very important for both front-end and operation.

What is browser caching

Browser caching (HTTP caching) means that the browser stores recently requested documents on a local disk so that when a visitor visits the same page again, the browser can load the documents directly from the local disk.

advantages

  1. Reduces redundant data transmission, saves bandwidth, and reduces server stress

  2. This improves client loading speed and user experience.

Strong cache

Strong caching does not send requests to the server, but reads resources directly from the Cache. Strong caching can be implemented by setting two HTTP headers: Expires and cache-Control, which are implementations of HTTP1.0 and HTTP1.1, respectively.

Expires

An Expires header is a header presented in HTTP1.0 that represents the expiration time of a resource. It describes an absolute time that is returned by the server. Expires is limited to local time, and if you change the local time, it invalidates the cache.

Cache-Control

Cache-control appears in HTTP/1.1. The common field is max-age in seconds, and many Web servers have a default configuration that takes precedence over Expires and represents a relative time.

For example, cache-control :max-age=3600 indicates that the validity period of the resource is 3600 seconds. Date in the response header, the time when the request is sent, indicates that the current resource is valid from Date to Date +3600s. Cache-control also has multiple values:

  • No-cache does not use caching directly, that is, it skips strong caching.
  • No-store prevents the browser from caching data and asks the server for a complete resource each time it requests a resource.
  • Public can be cached by all users, including end users and middleware proxy servers such as CDN.
  • Private only allows the end user’s browser cache, not other intermediate proxy server cache.

Note the difference between a no-cache and a no-store cache. A no-cache skips strong caching or negotiates a cache, whereas a no-store cache does not cache at all

Negotiate the cache

When a browser request for a resource does Not hit the strong cache, it sends a request to the server to verify that the negotiated cache is hit. If the negotiated cache is hit, the request response returns an HTTP status of 304 and displays a Not Modified string.

The negotiated cache is managed with last-modified, if-modified-since and ETag, if-none-match headers.

Attention!!!!! Negotiation Cache needs to be used together with strong Cache. Before using negotiation Cache, cache-control: no-cache or pragma: no-cache is set to tell the browser not to enforce strong Cache

The last-modified, If – Modified – Since

These two headers are from HTTP1.0, and the two fields are used together.

Last-modified indicates the date on which the local file was Last Modified. The browser will put if-modified-since in the request header. The server will match this value with the time when the resource was Modified. The last-Modified value is updated and returned to the browser as a response header. If the time is consistent, the resource is not updated. The server returns the 304 status code, and the browser reads the resource from the local cache after receiving the response status code.

But last-Modified has several problems.

  • The file has been modified, but the final content has not changed, so the file modification time is still updated
  • Some files are modified in seconds or less, so it is not enough to record them in seconds
  • Some servers cannot accurately obtain the last modification time of a file.

Hence the ETAG.

ETag, If – None – Match

In HTTP1.1, the server uses Etag to set the response header cache identifier. The Etag value is generated by the server. On the first request, the server returns both the resource and the Etag to the browser, which caches both to the local cache database. On the second request, the browser will put the Etag information in the if-none-match header to access the server. Upon receiving the request, the server will compare the file id in the server with the id sent by the browser. If the file id is different, the server will return the updated resource and the new Etag. The server returns the 304 status code, which the browser reads from the cache.

The process to summarize

To summarize these fields:

  • Cache-control — before requesting the server
  • Expires – Before a request to the server
  • If-none-match (Etag) — Request server
  • If-modified-since (last-modified) — Request server

The node practice

This article uses KOA as an example, because KOA is lighter, cleaner, and doesn’t bundle any middleware itself. Compared to Express, which comes with many router, static, and other middleware functions, KOA is a better example for this article.

Koa starts the service

In the interest of learning and making it easier to understand, a simple implementation of a Web server in KOA without using koA-static and KOA-Router middleware validates the previous conclusions.

  1. Create a project
Create a directory and create a new index.js file
mkdir koa-cache
cd koa-cache
touch index.js

Initialize the project
git init
yarn init

Install KOA as a local dependency
yarn add koa
Copy the code
  1. Koa code
/*app.js*/
const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    ctx.body = 'hello koa'
})

app.listen(3000.() = > {
  console.log('starting at port 3000')})Copy the code
  1. Start the service
node index.js
Copy the code

So a KOA service comes up, go to localhost:3000 and see Hello KOa.

To facilitate debugging, you do not need to restart the service to modify the code. You are advised to use nodemon or PM2 to start the service.

Native KOA implements simple static resource services

The key to implementing a static resource server is to determine the requested resource Type according to the address of the front-end request, set the returned content-Type, so that the browser knows the returned Content Type, and then the browser can decide what form and encoding to read the returned Content.

Define a list of resource types

const mimes = {
  css: 'text/css'.less: 'text/css'.gif: 'image/gif'.html: 'text/html'.ico: 'image/x-icon'.jpeg: 'image/jpeg'.jpg: 'image/jpeg'.js: 'text/javascript'.json: 'application/json'.pdf: 'application/pdf'.png: 'image/png'.svg: 'image/svg+xml'.swf: 'application/x-shockwave-flash'.tiff: 'image/tiff'.txt: 'text/plain'.wav: 'audio/x-wav'.wma: 'audio/x-ms-wma'.wmv: 'video/x-ms-wmv'.xml: 'text/xml',}Copy the code

Resolve the resource type of the request

function parseMime(url) {
  // path. extName Obtains the file name extension in the path
  let extName = path.extname(url)
  extName = extName ? extName.slice(1) : 'unknown'
  return mimes[extName]
}
Copy the code

Fs reads files

const parseStatic = (dir) = > {
  return new Promise((resolve) = > {
    resolve(fs.readFileSync(dir), 'binary')})}Copy the code

Koa treatment

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // Accessing the root path returns index.html
    ctx.set('Content-Type'.'text/html')
    ctx.body = await parseStatic('./index.html')}else {
    ctx.set('Content-Type', parseMime(url))
    ctx.body = await parseStatic(path.relative('/', url))
  }
})
Copy the code

This basically completes a simple static resource server. Then create a new HTML file and static directory under the root directory, and drop some files under static. The table of contents should look like this:

|-- koa-cache |-- index.html |-- index.js |-- static |-- css |-- color.css |-- ... |-- image |-- soldier.png |-- ... . .Copy the code

Localhost :3000/static to access the specific resource file.

index.html
<! DOCTYPEhtml>
<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta http-equiv="X-UA-Compatible" content="IE=edge" />
   <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
   <title>test cache</title>
   <link rel="stylesheet" href="/static/css/index.css" />
 </head>
 <body>
   <div id="app">Testing the CSS file</div>
   <img src="/static/image/soldier.png" alt="" />
 </body>
</html>
Copy the code
css/color.css
#app {
  color: blue;
}
Copy the code

Open localhost:3000 and you’ll see the following:

At this point, the basic environment is set up. Next comes the validation phase.

Strong cache

Before any configuration, take a look at network:

At this point, no matter for the first time or several times, the server will be asked for resources.

Attention!! Before starting the experiment, Disable cache is checked in the Network panel. This disables browser caching. Browser requests are Pragma: no-cache and cache-control: no-cache headers, so all requests are not cached

Set the Expire

Modify the app.use code in index.js.

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // Accessing the root path returns index.html
    ctx.set('Content-Type'.'text/html')
    ctx.body = await parseStatic('./index.html')}else {
    const filePath = path.resolve(__dirname, `.${url}`)
    ctx.set('Content-Type', parseMime(url))
    // Set the expiration time to 30000 milliseconds, i.e. 30 seconds later
    ctx.set('Expires'.new Date(Date.now() + 30000))
    ctx.body = await parseStatic(filePath)
  }
})
Copy the code

Use ctx.set(‘Expires’, new Date(date.now () + 30000)) to set the expiration time to 30000 milliseconds of the current time, 30 seconds later.

If you go to localhost:3000, you can see the Expires Header.

The CSS file displays the disk cache, while the image resource displays the FROM Memory cache. In this case, the browser is directly reading the browser cache, and did not request the server, you can try to change the name of the CSS and image file or delete the page to verify that the page is normal, indicating that the previous conclusion is correct.

Cache-Control

Ctx. set(‘ cache-control ‘, ‘max-age=300’) sets the validity period to 300 seconds.

Negotiate the cache

The if-modified-since last-modified

The HTTP1.0 negotiation cache key is to determine whether the resource has been updated based on the time of the ifModifiedSince field and the corresponding modification time of the requested resource.

Set cache-control: no-cache, so that the client does not Cache strongly, then check whether the client request has ifModifiedSince field, set last-Modified field, and return the resource file. If yes, use fs.stat to read the modification time of the resource file and compare. If the time is the same, return the status code 304.

 ctx.set('Cache-Control'.'no-cache')
 const ifModifiedSince = ctx.request.header['if-modified-since']
 const fileStat = await getFileStat(filePath)
 if (ifModifiedSince === fileStat.mtime.toGMTString()) {
    ctx.status = 304
 } else {
    ctx.set('Last-Modified', fileStat.mtime.toGMTString())
    ctx.body = await parseStatic(filePath)
 }
Copy the code

Etag, If – None – Match

The key point of eTAG is to calculate the uniqueness of the resource file. Here, nodeJS’s built-in crypto module is used to calculate the hash value of the file and represent it as a hexadecimal string. The use of CYPTO can be found at nodejs. Crpto supports not only string encryption, but also incoming buffer encryption, which, as a built-in nodeJS module, is the perfect place to calculate a unique identifier for a file.

    ctx.set('Cache-Control'.'no-cache')
    const fileBuffer = await parseStatic(filePath)
    const ifNoneMatch = ctx.request.headers['if-none-match']
    const hash = crypto.createHash('md5')
    hash.update(fileBuffer)
    const etag = `"${hash.digest('hex')}"`
    if (ifNoneMatch === etag) {
      ctx.status = 304
    } else {
      ctx.set('etag', etag)
      ctx.body = fileBuffer
    }
Copy the code

On the second request, the browser will add if-none-match, and the server will calculate the hash value of the file and compare it again. If the file is the same, 304 will be returned. If the file is different, a new file will be returned. If you change the file, the hash value of the file changes, and the two hashes do not match, the server returns the new file with the hash value of the new file as an etag.

summary

The above code practices the effect of each cache field. As a demonstration, the production of static resource servers will be more complex. For example, eTAG will not refetch the file every time to calculate the hash value of the file, which costs too much performance. For example, index caching of last-Modified and ETAG values of resources.

conclusion

Usually web servers have default cache configuration, the specific implementation may not be the same, like Nginx, Tomcat, Express and other Web servers have corresponding source code, interested can go to read and learn.

The proper use of strong cache and negotiated cache depends on the application scenario and requirements of the project. Like the current common single-page application, because the packaging is usually newly generated HTML and the corresponding static resource dependency, so you can configure negotiation cache for HTML files, and the packaging generated dependencies, such as JS, CSS files can use strong cache. Or use strong caching only for third party libraries, which typically have slower version updates and can be locked.

The complete code for the Node example can be viewed here at github.com/chen-junyi/…

Write in the last

My writing level is limited, where to say wrong and write wrong welcome to point out, there is any problem also welcome to leave a message below communication.

I recently prepared to organize a front-end knowledge document website, interested in organizing together can leave a message, specific can see the first article below

Public number: have fun at the front end

Personal website: Chen-junyi.github. IO /article/

The articles

  • Automate the deployment of your documentation site with GitHub Action + VuePress
  • NextTick is fully resolved in VUE