Website Performance Optimization: A Guide to Boosting Page Speed with Easy Caching Methods

We all want to take care of our users and provide them with the best possible user experience(UX). Caching plays one of the important roles in improving the user experience.

Difficulty Level: Beginner

Prerequisite: You should be familiar with single page application and HTTP headers

Takeaways: This article gives you a overview about simple caching technique in single page applications

Website performance optimization

Why do we need caching?

Web application loads various assets like html, css, js, images.. Etc. to the frontend, these assets are static and not change for sometime or throughout the day or till a certain time period. So, reloading the asset every time from a remote server involves bandwidth and time. To reuse the same asset after first load there are various techniques to cache it, serving the asset from cache gives better user experience and improves page load speed.

There are dynamic data loads in web applications from api responses(GET, PUT, POST, PATCH, DELETE, ...) these data can also be cached using certain techniques

How?

Static Response

Static assets are responsible for primary display content of web pages, caching the static assets like HTML, js, css, images enhance user experience. The simple way to cache static assets is HTTP caching through the 'max-age' property in HTTP headers. By setting appropriate 'max-age' values for static assets, you can instruct the browser to cache these resources for a specified duration. This reduces the need for repeated downloads, significantly improving page load times and decreasing server load.

Web Development

Here's an example of how you can use the max-age property in HTTP headers for static assets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Express.js example (Node.js server)
const express = require('express');
const path = require('path');

const app = express();

// Serve static assets with caching headers
app.use('/static', express.static(path.join(__dirname, 'public'), {
  maxAge: '7d', // Set the max-age to 7 days
}));

// Your other routes and middleware...

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

In this example, the maxAge property is set to '7d', indicating that the static assets will be cached in the browser for seven days. Adjust the value according to your application's requirements and update frequency for static assets.

By incorporating max-age caching headers, you not only enhance the performance of your SPA by reducing latency but also contribute to a smoother user experience, especially for returning visitors who can benefit from cached resources without re-downloading them on subsequent visits.

Dynamic Response

In single page applications(SPA), dynamic data are majorly from API. In this article we consider Axios as HTTP client and learn about API response caching in the rest of the article.

Axios client has interceptors which is a powerful tool that empowers developers to take control of their HTTP requests and responses. By leveraging interceptors, you can implement implement caching layer in single page applications.

Caching API data using Axios interceptors can significantly improve the performance of your application by reducing redundant network requests. Axios interceptors allow you to intercept and modify HTTP requests and responses, providing an opportune moment to implement caching mechanisms. Below is an example of how you can use Axios interceptors to cache API data in a simple way:

Cache-handler.js

  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
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import Dexie from 'dexie';
import axios from 'axios';
import momentTimezone from 'moment-timezone';

// initialize constants
const indexedDbExists = ('indexedDB' in window);
const dbName = 'DynamicCaching';
const cacheVersion = parseInt(process.env.VUE_APP_CACHE_VERSION);
const cacheEnabled = parseInt(process.env.VUE_APP_ENABLED) ? true : false;
const cacheDb = new Dexie(dbName);
let isUpgraded = false;
const defaultTimestamp = setDefaultTimestamp();

if (!cacheEnabled) {
  cacheDb.delete();
}

async function getVersionNo() {
  try {
    let db = await cacheDb.open();
    return db.verno;
  } catch(err) {}
}

async function initDb() {
  cacheDb.close();
  return cacheDb.version(cacheVersion).stores({
    thCacheStore: 'url, response, maxAge, timestamp'
  });
}

function isWhitelisted(url) {
  return ![
    '/live_usd_to_sgd',
    '/live_gold_price'
  ].includes(url);
}

function handleRequest(request) {
  return cacheDb.thCacheStore.where("url")
    .equals(request.baseURL + axios.getUri(request))
    .first(cacheData => {
      if (cacheData) {
        let estTime = momentTimezone().tz('America/New_York');

        if ((estTime.diff(cacheData.timestamp)/1000) > cacheData.maxAge) {
          request.headers.cacheNow = true;
          cacheDb.thCacheStore
            .delete(request.baseURL + axios.getUri(request));
          return Promise.resolve(request);
        } else {
          request.headers.cached = true;
          return Promise.reject(request);
        }
      } else {
        request.headers.cacheNow = true;
        return Promise.resolve(request);
      }
    });
}

export function request(request) {
  if (cacheEnabled 
    && (request.method === 'GET' || 'get') 
    && isWhitelisted(request.url)
    && indexedDbExists) {
    if (!isUpgraded) {
      return getVersionNo()
        .then((version) => {
          isUpgraded = true;
          if (version != cacheVersion) {
            cacheDb.delete();
            return initDb()
              .then(() => {
                return cacheDb.open().then(() => {
                  return handleRequest(request); 
                });
            });
          } else {
            return initDb()
              .then(() => {
                return cacheDb.open().then(() => {
                  return handleRequest(request); 
                });
              });
          }
        });
    } else {
      return initDb()
        .then(() => {
          return cacheDb.open().then(() => {
            return handleRequest(request); 
          });
        });
    }
  } else {
    return Promise.resolve(request);
  }
}

export function response(serverResponse) {
  cacheDb.thCacheStore.put({
    url: axios.getUri(serverResponse.config),
    response: JSON.stringify(serverResponse.data),
    maxAge: 86400,
    timestamp: defaultTimestamp,
  });
}

export function serveFromCache(request) {
  return cacheDb.thCacheStore
    .where({
      url: request.baseURL + axios.getUri(request)
    })
    .first(cachedResponse => {
      return Promise.resolve(JSON.parse(cachedResponse.response));
    });
}

function setDefaultTimestamp() {
  let estTime = momentTimezone().tz('America/New_York');
  let formattedDateTime = "";
  estTime.format();

  if (estTime.hour() < 7) {
    formattedDateTime = estTime.subtract(1, "days").format();
  } else {
    formattedDateTime = estTime.format();
  }
  
  let [date, time] = formattedDateTime.split('T');
  let splitDiff = time.split('-')[1];

  return `${date}T07:00:00-${splitDiff}`;
}

Axios.js

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import axios from 'axios';
import * as cacheHandler from './cache-handler';

const http = axios.create({
  baseURL: process.env.VUE_APP_BACKEND_URL,
  headers: {
    "Accept": "application/json",
    "Content-Type": "application/x-www-form-urlencoded",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, PATCH, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Origin, Content-Type, X-Auth-Token"
  }
});

/* Response interceptors */
const interceptResponseErrors = (err) => {
  try {
    if(err.headers.cached) {
      return cacheHandler.serveFromCache(err);
    }
    err = Object.assign(new Error(), {message: err.response.data});
  } catch(e) {
    // Will return if something goes wrong
  }
  return Promise.reject(err);
};

const interceptResponse = (res) => {
  try {
    if (res.config.headers.cacheNow) {
      cacheHandler.response(res);
    }
    return Promise.resolve(res.data);
  } catch(e) {
    return Promise.resolve(res);
  }
}

http.interceptors.response.use(interceptResponse, interceptResponseErrors);

/* Request interceptors */
const interceptRequestErrors = err => Promise.reject(err);
// const interceptRequest = config => config;
function interceptRequest(request) {
  if (request.method === 'GET' || 'get') {
    return cacheHandler.request(request)
      .then(cacheRequest => cacheRequest)
      .catch(cacheRequest => {
        if (cacheRequest.headers && cacheRequest.headers.cached) {
          return Promise.reject(request);
        } else {
          request.headers.cacheNow = true;
          return Promise.resolve(request);
        }
      });
  } else {
    return request;
  }
}

http.interceptors.request.use(interceptRequest, interceptRequestErrors);

export default http;
In this example:
  1. The above example uses IndexedDb in browser to cache the api responses locally in browser
  2. Used dexie.js library to interact with the indexedDb in browser
  3. The cache is applied only to HTTP GET requests, because POST, PATCH, DELETE and PUT requests are used for write operations
  4. Few get endpoints response data changes frequently, for example live gold price, which changes based on market trends, which can be blacklisted in method isWhitelisted method
  5. Setting additional metaData like ‘maxAge’, ‘timestamp’ to invalidate the cache within certain time period

This basic caching strategy can be expanded and customized based on your application's requirements. Keep in mind that this example uses an indexedDb cache for large scale applications.

Conclusion

Ultimately, the HTTP caching and Axios interceptors depend on the nature of the content. For static assets, leveraging the simplicity of HTTP caching headers is a pragmatic choice. In contrast, the flexibility of Axios interceptors shines when dealing with dynamic data fetched from APIs.

By combining these caching strategies judiciously, developers can strike a balance between efficient resource utilization and a seamless user experience. Whether building static websites, single-page applications, or dynamic web platforms, a thoughtful approach to caching contributes to a faster, more responsive, and user-friendly web application. As the digital landscape continues to evolve, optimizing for speed and performance remains a constant priority for developers aiming to deliver top-notch web experiences.

If you're looking to optimize your web applications with expert techniques our team at Meant4 is here to help. With our deep expertise in web development and API integration, we can elevate your projects and deliver tailored solutions. Contact us today to learn how we can support your development needs!

Written by:

 avatar

Chandra Sekar

Senior Frontend Developer

Chandra is an exceptional Vue.js frontend developer who consistently delivers outstanding results. With a keen eye for detail and a deep understanding of Vue.js, Chandra transforms design concepts into seamless and visually captivating user interfaces.

Read more like this: