Server-Side Caching in Expressjs

By Chidume Nnamdi 🔥💻🎵🎮

Caching has always been one of the many optimization tricks widely used in software development. In this post, we will look at how to enable server-side caching in Expressjs. Read on.

Web apps often access the server to get new data to render in the DOM. This puts a heavy load on the server since it requires heavy processing power to produce dynamic results.

Browsers on their side make use of caching to prevent frequent server requests. Browsers have an internal caching mechanism that enables them to store responses of requests and to respond with the cached response when the same request is made. this helps to reduce frequent server operations from the same requests.

Though browsers help but the server needs to run and render for different browsers and users. The server-side has to find a way to optimize long operations. This is done through the use of caching.

Before we delve into server-side caching let’s first understand what caching means.

Caching is the concept of storing the result of a CPU-intensive operation so that in the next execution of the same operation with the same input, the result is returned from the storage instead of performing the resource-consuming operation all over again.

In programming, functions are usually cached and this process is called memoization.

function wait(ms) {
var start = new Date().getTime()
var end = start
while(end < start + ms) {
end = new Date().getTime()
}
console.log('done with longOp func')
}

function longOp(input) {
let result = null
// simulating a long operation
wait(1000)
result = 90 * input
return result
}
longOp(5) // takes 1000 ms to complete
longOp(50) // takes 1000 ms to complete
longOp(500) // takes 1000 ms to complete
longOp(5000) // takes 1000 ms to complete
// Total ms to run: 4000 ms!
longOp(5) // takes 1000 ms to complete
longOp(50) // takes 1000 ms to complete
longOp(500) // takes 1000 ms to complete
longOp(5000) // takes 1000 ms to complete
// Total ms to run: 4000 ms! again

The function above takes approx, 1000 milliseconds to run. We tried calling it with different inputs: 5, 50, 500, 5000. In total it will take 4000 ms for our file to run !! Running the same inputs again will take another 4000 ms!!

Now, since we know that the output depends on the input return input * 90. We can store the result for each input and return the stored result on the next invocation with the same input.

// ...
const cache = {}
function longOp(input) {
let result = null

// check if the input has been calaculated already and stored in cache
if(cache[input]) {
// return the result in the cache bypassing the long operation
return cache[input]
}
// if not do the operation and store the result
else {
// simulating a long operation
wait(1000)
result = input * 90
// store the result with the input as key
cache[input] = result
}
return result
}

Now, we have modified our longOp function to use caching. The cache object is where we store the result of prev. calc. of an input. When the function is called with an input, we check if the results in the cache object, if yes we return the result bypassing the long operation. If no, we perform the long operation and store the result in the cache object with the input as the key, so on next invocation with the same input, the result will be found in the cache object.

If we run the longOp again:

longOp(5) // takes 1000 ms to complete
longOp(50) // takes 1000 ms to complete
longOp(500) // takes 1000 ms to complete
longOp(5000) // takes 1000 ms to complete
// Total ms to run: 4000 ms!
longOp(5) // takes 1 ms to complete
longOp(50) // takes 1 ms to complete
longOp(500) // takes 1 ms to complete
longOp(5000) // takes 1 ms to complete
// Total ms to run: 4 ms! again :)

You see that is a huge performance boost!! from 4000ms to 4ms!! The first part ran without from the cache, the second part just returned the result of the cache operation from the first part.

Now, we have understood what caching means, let’s move on.

We experience the same thing when using Expressjs, a Nodejs framework for a quick scaffold of servers. Let’s say we have an index route like this:

app.get('/', (req, res) => {
// simulating a long process
wait(1000)
res.send('message from route /')
})

We have a route here that will return us the message message from route /, we used setTimeout to simulate a slow process, this is so if we are generating close to a thousand news if we are a news website server API. This takes close to 1000 ms to deliver the news payload to our users.

To test this out, start a Node project and install the express module:

mkdir expr-cache-prj
cd expr-cache-prj
npm init -y
npm i express
touch index.js

Paste the following in the index.js file:

const express = require('express')
const app = express()
const log = console
app.get('/', (req, res) => {
// simulating a long process
wait(1000)
res.send('message from route /')
})
app.listen(3000, () => console.log('Server: 3000'))

Run node ./ in your terminal to start the server:

node ./
Server: 3000

Open your fav browser and navigate to localhost:3000. Open your DevTools and click on the Network tab:

You see in the timeline tab it took approx 1090 ms for our route / to render this due to the time 1000ms it took for the server to respond and the remaining 90ms is what it to render the message. You see how the server can impact on the performance of our web apps. Optimizing not only our client-side but also importantly our server-side app is highly important. Optimize the client-side the way you can even up to 0.000001ms if your server-side app takes up to 1000ms to send data your client will take (1000 + 0.000001) 1000.000001 ms to render.

If we up the ms to 9000:

app.get('/', (req, res) => {
// simulating a long process
wait(1000)
res.send('message from route /')
})

We will get:

Here it took 9074 ms for the message to render. Our server took the bulk of 9000ms from the 9074ms to send the message.

Now, let’s see different techniques used to cache Expressjs server.

In Expressjs, middleware is the ideal place to add your caching logic.

const middleWare1 = (req, res, next) => {
next()
}
const middleWare2 = (req, res, next) => {
next()
}
app.get(route, middleWare1, middleWare2)

When the route route is navigated to, Express calls middleWare1, as the middleWare1 calls next(), Expressjs calls the next middleware in line, middleWare2, middleWare2 calls next() as there is no more middlewares inline the execution exits.

Expressjs
|
v
route match
|
v
middleWare1
|
v
middleWare2

Normally, in Expressjs we send data back to the user in the last middleware:

const middleWare1 = (req, res, next) => {
next()
}
const middleWare2 = (req, res, next) => {
res.send('Expressjs data')
}
app.get(route, middleWare1, middleWare2)

We see its perfect fit if we add the caching logic in the middleWare1. It will be first to run so we check if the route has been cached, if yes, we send the cached result from the middleware, if not we bubble it down to the middleWare2.

In our / route:

app.get('/', (req, res) => {
// simulating a long process
wait(1000)
res.send('message from route /')
})

We will add a middleware which will hold our caching code.

var cache = {}
var midWare = (req, res, next) => {
const key = req.url
if (cache[key]) {
res.send('from cache')
} else {
res.sendResponse = res.send
res.send = (body) => {
cache[key] = body
res.sendResponse(body)
}
next()
}
}
app.get('/', midWare, (req, res) => {
// simulating a long process
wait(1000)
res.send('message from route /')
})

We created a middleware midWare. We created a cache object which will hold our caches, in the midWare function, we use the request URL string as the key in our cache object. first, we check if the cache based on the key req.url exists in the cache object, if yes we send the response from the cache to the user if no we store the body to the cache object and send the response so next time the response will be from the cache object.

If we run our server with the above code we will see improvement in our browser:

See, it took our browser 23ms to render!! more than half of our previous result.

We have seen how to add caching to our Express server. There are downsides to what we did above:

1. We stored the cache object in the process, so when our server goes down Ctrl + C we will start caching from scratch again.

2. The cache can never be shared among other multiple servers in the same process.

NB Caching is feasible in GET routes, NEVER add caching to PUT, DELETE, POST routes. In the GET routes caching should be added when the input depends on the output, GET routes that side-effects should never be cached because the output will change with time.

Redis is a nontraditional database, dubbed a data structure server, which functions in operational memory with blazingly fast performance.

Redis is a very powerful in-memory data store that we can use in our apps as a cache or as a database. Its primary API consists of set(key, value) and get(key). To integrate redis into our existing app, we begin by installing the redis module.

First, we install the redis npm module:

npm i redis

Next, we create the redis module and establish a connection to the redis server.

const redis = require('redis'),
client = redis.createClient()

We edit the exp.js file to look like this:

const redis = require('redis'),
client = redis.createClient()

client.on('connect',()=>log('Redis connected'))

const express = require('express')

const app = express()
const log = console

var midWare = (req, res, next) => {
const key = req.url
client.get(key, (err, result) => {
if (err == null && result != null) {
res.send('from cache')
} else {
res.sendResponse = res.send
res.send = (body) => {
client.set(key, body, (err, reply) => {
if (reply == 'OK')
res.sendResponse(body)
})
}
next()
}
})
}
app.get('/', midWare, (req, res) => {
// simulating a long process
wait(1000)
res.send('message from route /')
})

Same with our prev. code, we just edited it to use Redis instead. The client.get(key, cb) retrieves a stored value from the store with reference to the passed in key. The cb is a function that runs when the operation completes, it takes two params, the first is an Error object and the sec is the result of the get operation. So we set up our code in the cb function, we check if the err param is null and the result param is not null if it passes that means a value was cached previously and found in the store so we send the cache. if not, we set the store with the key and call the next middleware in the chain.

Using Memcached, we will install memjs npm module:

npm i memjs

and configure it in our exp.js

const memjs = require('memjs')
const mc = memjs.Client.create()
const express = require('express')

const app = express()
const log = console

var midWare = (req, res, next) => {
const key = req.url
mc.get(key, (err, val) => {
if (err == null && val != null) {
res.send('from cache')
} else {
res.sendResponse = res.send
res.send = (body) => {
mc.set(key, body, { expires: 0 }, (err, reply) => {
if (reply == 'OK')
res.sendResponse(body)
})
}
next()
}
})
}
app.get('/', midWare, (req, res) => {
// simulating a long process
wait(1000)
res.send('message from route /')
})

Same as Redis, the only thing that changed is the client to mc. Redis and Memcached have almost the same API with the same functionality.

We saw what server-side caching in Expressjs is all about and how to configure one using custom code. Next, we saw how to use Redis and Memcached to add caching to our Expressjs server.

Caching is one of the greatest optimization tricks we have to speed up our apps especially apps on the server side. Many have called it different names: memoization, it all boils down to the same idea of caching.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment below and ask me anything! Thanks !!!