Skip to main content

How to Optimize Node Requests with Simple Caching Strategies

One of the things that affect how users interact with our applications is its speed. Even though some users generally have a poor connection, they are expecting some minimum level of speed when using the application. To give our users a seamless experience, we are going to consider the possibility of optimizing our requests in node by using the concept of caching.

Trust me, your users will thank you.

In this article, we are going to look at how to optimize requests in our Node applications by caching responses to requests. Now, you may be wondering what caching is all about. Caching generally involves storing and retrieving data from a high-performance memory

  • Node installed on your machine,
  • Node Package Manager ( NPM ) installed on your machine
  • Some Basic Javascript Knowledge

To confirm your installations, run the following in your terminal :

    node --version
npm --version

If you get version numbers as result, then you’re good to go.

Building A Basic Inventory Application

We need to make a simple inventory reporting application to test caching capabilities with. The main goal is to look at the different approaches to caching in our node applications.

Since you already have npm installed on your machine, create a new project folder and install the necessary packages :

    mkdir inventory-app
cd inventory-app 
npm init && npm install --save express memory-cache flat-cache redis sqlite3

Let’s take a look at the index.js.

Import express and sqlite:

    // index.js
const express = require('express')
							const sqlite3 = require('sqlite3').verbose();
							const PORT = process.env.PORT || 3128 ;
							const app = express();
[...]

express is a routing framework for Node which simplifies HTTP task. sqlite3 will power the database for this app. The application port is also specified and an express application is created.

Next thing we need to do is to create the server route. Just one route that points to a /products endpoint:

    // index.js
[...]
						
app.get('/products',  function(req, res){
	setTimeout(() => {
	  let db = new sqlite3.Database('./NodeInventory.db');
	  let sql = `SELECT * FROM products`;
						
	  db.all(sql, [], (err, rows) => {
		  if (err) {
			  throw err;
		  }
		  db.close();
		  res.send( rows );
	  });
	  // this was wrapped in a setTimeout function to intentionally simulate a slow 
	  // request
	}, 3000);
});
						
[...]

When a request is made to the /productsroute, a connection is made to our sqlite database and then the query’s executed to fetch all our products. Once this is done, the connection is closed and the response is sent back to the user with all the products that exist in our inventory.

The sqlite file is in the Github repository for reference.

To start listening to requests on the port, add the following at the end:

    // index.js
[...]
						
app.listen(PORT, function(){
	console.log(`App running on port ${PORT}`);
});
						
[...]

Head to Postman or the browser and visit the /products route to get a list of the items stored in the database:

First run without caching gave a response time of 3043ms

So far our single-route app works, but imagine having to do this over and over again which is what will be the case when our daily inventory is being reviewed over a hundred times. Let’s take a look at how to optimize requests by making use of some caching functions

Different Caching Options

Using Memory-Cache for caching ( In - Memory ) Earlier on, the memory-cachemodule was installed. Now, let’s look at how to use it.

Import the module:

    // index.js
[...]
						
cache cache = require('memory-cache');
						
[...]

Create and configure cache middleware :

The duration representing how long the values need to stored in the cache will be specified. In the middleware, a unique key based on the request url is generated and a check is made to see if there is already content stored for that key. If content exists, the data is sent back as the response without having to make the extra query to our database.

Now, if there is no content in the cache for the particular key, the request is processed as usual and the result of the request is stored in our cache before the response is sent to the user.

To use the cacheMiddleware edit the index.jsto look like this :

    // index.js
app.get('/products', cacheMiddleware(30), function(req, res){
								 [...]    
							});

Notice how specified the duration we want the response for the request to be cached for was specified

We just looked at in-memory method for caching responses. One of the downsides of this method is that once the server goes down, all cached content is lost.

Returns Products with a response time of 3045ms

Now, let's take a look at the using a caching method that persists the data on the server even after it has been restarted.

Using Flat file for caching We are going to now take a look at how to use files to persist our cached data on the server. To do this, we are going to make use of this package - flat-cache

Install the module by running :

    npm install --save flat-cache

Now, tweak the index.js to make use of our new module. The index.js will look like this :

    // index.js
[...]
						
const flatCache = require('flat-cache')
						
[...]

Then the new cache is loaded

    // index.js 
[...]
						
// load new cache
let cache = flatCache.load('productsCache');
// optionally, you can go ahead and pass the directory you want your
// cache to be loaded from by using this
// let cache = flatCache.load('productsCache', path.resolve('./path/to/folder')
						
[...]

Now that the cache has been loaded, the next thing to do now is to edit our application route so :

    // index.js
[...]
						
// create flat cache routes
let flatCacheMiddleware = (req,res, next) => {
	let key =  '__express__' + req.originalUrl || req.url
	let cacheContent = cache.getKey(key);
	if( cacheContent){
		res.send( cacheContent );
	}else{
		res.sendResponse = res.send
		res.send = (body) => {
			cache.setKey(key,body);
			cache.save();
			res.sendResponse(body)
		}
		next()
	}
};
						
// create app routes 
						
[...]

The logic behind this is similar to the in-memory cache. Check if a response already exists for our request using the key and it there already exists a response, it is returned to the user else, the request is executed and the response is saved to the cache before going to the user.

Notice that we ran the cache.save() . Doing this specifies that the cache is saved as a file in the directory we specified - if you specified any. If no directory was specified the module will determine where to save the ‘cache-file’

To use the flatCacheMiddleware , edit the application routes so :

    // index.js
[...]
						
// create app routes
						
app.get('/products', flatCacheMiddleware, function(req, res){
  [...]
});

Now, when the /products route is visited, the following result is obtained :

Products are returned, with a response time of  3047ms

productsCache file is also created with the following content :

    {"__express__/products":"[{\"ID\":1,\"NAME\":\"Shoes\",\"COUNT\":20},{\"ID\":2,\"NAME\":\"Bags\",\"COUNT\":12},{\"ID\":3,\"NAME\":\"Tables\",\"COUNT\":50},{\"ID\":5,\"NAME\":\"Laptop\",\"COUNT\":140},{\"ID\":6,\"NAME\":\"Chair\",\"COUNT\":200},{\"ID\":7,\"NAME\":\"Mouse\",\"COUNT\":20},{\"ID\":8,\"NAME\":\"Pen\",\"COUNT\":1000},{\"ID\":9,\"NAME\":\"Chips\",\"COUNT\":6303},{\"ID\":10,\"NAME\":\"Slides\",\"COUNT\":22},{\"ID\":11,\"NAME\":\"Console\",\"COUNT\":32}]"}

Using MemCached ( A Service ) Another option to consider for caching is memcachedMemcached is a caching client built for node JS with scaling in mind.

To use the Memcached node client, you need to have memcached installed on your machine. Head over here to get it installed. When you have it installed, you then install the memcached node client by running :

    npm install --save memcached

Now that the client is installed, import the module in the index.js:

    // index.js 
[...]
						
const Memcached = require('memcached');
						
[...]

Now, go ahead and configure the memcachedMiddleware by tweaking the index.js to look as follows :

    // index.js
[...]
						
let memcached = new Memcached("127.0.0.1:11211")
						
let memcachedMiddleware = (duration) => {
	return  (req,res,next) => {
	let key = "__express__" + req.originalUrl || req.url;
	memcached.get(key, function(err,data){
		if(data){
			res.send(data);
			return;
		}else{
			res.sendResponse = res.send;
			res.send = (body) => {
				memcached.set(key, body, (duration*60), function(err){
					// 
				});
				res.sendResponse(body);
			}
			next();
		}
	});
}
};
						
[...]

memcached object is created and the object is connected to the PORT our Memcached instance is running on - in this case, 11211. Afterwards, a memcachedMiddleware is also created which is similar to the other middlewares created earlier. If there is no content for the specified key in the cache, the request is completed as usual and the response is stored in our cache. If there is a response for the specified key, the content is obtained from the cache and returned to the user without having to process the request.

Now, edit the /products route and update it to use the memcachedMiddleware :

    // index.js
[...]
						
app.get("/products", memcachedMiddleware(20), function(req, res) {
  [...]
});
						
[...]

The memcachedMiddleware(20) above, specifies the duration ( in minutes ) for how long the key is to be stored in the cache.

When a request is made to the /products route from Postman, the following result is obtained :

Returns the products with a response time of 3049ms

Using Redis for caching Now, we have seen how to use in-memory cache and also how to persist our caches using files. Let’s take a look at using redis for caching in our application. Redis stands for **RE**``mote**D**``ictionary **S**``erver that has the ability to store and manipulate high-level data types.

To get started using redis for caching application, install the redis node client by running :

    npm install -save redis

We also need to have a redis server running on our local machine. You can head over here to find out how.

When you are sure your redis server is installed and working properly, the next thing to do is to import the necessary modules and create your redis client:

    // index.js
[...]
						
const redis = require('redis')
const client = redis.createClient();
						
[...]

After this, create a redisMiddleware and that’ll look like this :

    // index.js
[...]
						
// create redis middleware
let redisMiddleware = (req, res, next) => {
  let key = "__expIress__" + req.originalUrl || req.url;
  client.get(key, function(err, reply){
	if(reply){
		res.send(reply);
	}else{
		res.sendResponse = res.send;
		res.send = (body) => {
			client.set(key, JSON.stringify(body));
			res.sendResponse(body);
		}
		next();
	}
  });
};
						
// app routes
[...]

A check is made to see if a response already exists for our request. If a response exists, then we fetch the value from the cache and return the response. If no response exists in the cache, then the request is executed the response is stored in the redis cache for future requests.

The /products route is also updated to use the redisMiddleware :

    // index.js 
[...]
						
app.get("/products", redisMiddleware, function(req, res) {
  [...]
});
						
// set port
[...]

When a request to the /products server, you get :

Returns the products with a response time of 3049ms

Results and Recommendation


Now, we are going to compare performance for the different approaches we have taken in this article :

  • No Caching at all
  • In-Memory Caching
  • Caching to File
  • Using Memcached
  • Using Redis for Caching
  • No Caching at All
RunResponse Time
Ist Run3043ms
2nd Run3027ms
3rd Run3019ms
4th Run3009ms
Avg Time3024.5ms
  • In-Memory Caching
RunResponse Time
Ist Run3045ms
2nd Run23ms
3rd Run4ms
4th Run17ms
Avg Time772.25ms
  • Caching To File
RunResponse Time
Ist Run3047ms
2nd Run31ms
3rd Run8ms
4th Run13ms
Avg Time774.75ms
  • Using MemCached
RunResponse Time
Ist Run3049ms
2nd Run9ms
3rd Run10ms
4th Run16ms
Avg Time771ms
  • Using Redis for Caching
RunResponse Time
Ist Run3049ms
2nd Run6ms
3rd Run18ms
4th Run10ms
Avg Time770.75ms

From the results above, we can see that deciding not caching the files at all does not help improve the response time. The different caching options we have looked at so far have proven to be successful in reducing the average load time.

Conclusion

Caching comes in handy a lot of times, but you need to know when to use it. it may be an overkill especially when the requests made to the application aren’t frequent. Also, POSTPUT and DELETE methods should never be cached because you want unique resources to be affected each time a request is made.













Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...