360° View of Web Cache Solution — Part 1

Prasad Jayakumar
JavaScript in Plain English
5 min readJun 8, 2021

--

Caching strategies have to be chosen based on the kind of content, the change frequency, and user experience/expectation related to the transaction completed.

The benefits of caching any data should be rated based on the following criteria

  1. Prevent unnecessary network calls
  2. Prevent unnecessary load to the backend servers — Web/API Server, DB Server, and any other third-party systems
  3. Prevent unnecessary data transfer

TL;DR

  • Partial 1 — On every refresh, the browser makes a network call and passes ETag. The network call has happened, and the data gets transferred only if ETag differs, thus the Browser gets partially benefitted.
  • Partial 2 — On every call, the Web/API server needs to build the response and calculate the ETag. The network call has happened, and the data gets transferred only if ETag differs, thus the Web/API server gets partially benefitted.

For a better response time, implement ETag and pick a good max-age (expiration). If your goal is to lower the burden of the DB Server or third-party system, then include a Cache Server in the landscape. Cache servers save you some dollars and your app reputation.

Web application involves the following kind of content

  • Static assets like JS, CSS, images, fonts, etc. typically change during NEW releases. Change frequency depends on your release cycle. These assets can be cached for days or even years.
  • Dynamic contents like HTML (server-side) and API responses can change frequently. Master data are slow-changing entities and can be cached for a few minutes. Transactional entities have to be in sync up to the minute or seconds.

The browser and intermediary caches (CDN or Proxy Servers) use the following HTTP headers for better cache management.

  • Cache-Control — no-store, no-cache, public/private, must-revalidate, and max-age(expiration)
  • Cache validation headers— ETag (If-None-Match) and Last-Modified (If-Modified-Since)

Static Assets (of any Single Page Application)

Webpack helps to create a content hash for each of the bundles and include them in the bundle file name. These versioned resources can be cached, for a year, without worrying about staleness issues. Resource names, along with their public path, are included in the HTML file.

To avoid unnecessary change during the regeneration of the bundle file, we need to identify and split the bundle. For example, vendor-specific chunks, route (lazyload) specific chunks, etc, should be created as a separate bundle., For more details, refer to the Webpack Guide

Vue CLI build output for production based on the default configuration

Web Server

For simplicity, let us host the bundled resources using Express Server. Concepts hold good for Nginx and any other web servers.

const express = require('express');
const app = express();
const port = 3000;
app.use(express.static('dist'));app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

The following behavior will be observed in the Chrome browser when we access the home page.

  • On the first visit to the home page, all static resources will be downloaded
  • On subsequent visits, the browser checks with the Web server if the content got changed using ETag.
Chrome browser network flow

Express Server (by default)

  • Auto-generates weak ETag for all HTTP response
  • Sends Last-Modified of all static assets in an HTTP response

Chrome Browser

  • Sends the If-Modified-Since whenever Last-Modified is available in the cached response
  • Sends the If-None-Match whenever ETag is available in the cached response

End-users will not get benefitted from this approach unless the response involved a few MBs or more. Rather, preventing unnecessary network calls could provide greater benefits.

Setting maxAge for all assets except HTML files

Express Server for Static Assets

For the given maxAge equal to 1 year, the browser would start caching and prevent network calls. Versioned resource URLs avoid staleness issues.

Browser picks the response from the memory cache

API Server

For simplicity, let’s use Express Server for publishing API. The concept holds good for SpringBoot, PHP, or any other frameworks/languages.

Sample Router — Cities
// Vue code fragment
created
() {
axios.get("/api/cities").then((resp) => {
console.log(`Response recieved: ${resp.status}`, resp);
this.cities.push(resp.data);
});
},
Chrome browser network flow

Express Server (by default)

  • Auto-generates weak ETag for all HTTP response
  • Unless the response payload is generated, the Express server would not auto-generate ETag. In a production scenario, the payload would be available only after making a database or third-party system call.

Chrome Browser

  • Sends the If-None-Match whenever ETag is available in the cached response
  • If-None-Match is not added explicitly in the Axios call, rather the browser sends additional headers by default
  • Even though the browser received HTTP 304 as a response, Axios call will receive HTTP 200 along with payload picked from the browser cache

Overall this flow provides limited benefit for Web Server and end-users. There are no benefits to the database server / third-party system.

We need to introduce maxAge based on the acceptance level of staleness (no one-size-fits-all). This setting would provide some relief to the backend servers.

// sample code 
router.get
('/cities', (_, res) => {
res.set('Cache-control', 'public, max-age=300');
res.json(cities);
});

Cache-Control: no-cache for resources that should be revalidated with the server before every use.
Cache-Control: no-store for resources that should never be cached.
And the ETag or Last-Modified header can help you revalidate expired cache resources more efficiently.

Source: Prevent unnecessary network requests with the HTTP Cache

Conclusion

The caching strategy should be included in your architecture and design considerations from the beginning of your app development. Implementation can be handled when it fits your time and budget. Hope the information shared provides a perspective. Share your ideas or suggestions through comments.

If you like the idea, I will share my thoughts related to Cache Server and Service Worker in Part 2, until then happy coding.

--

--