Skip to main content

Working with the JavaScript Cache API

Introduction

The Cache API provides a mechanism for storing network requests and retrieving their corresponding responses during run-time. It can be used in the absence of an internet connection (or presence of a flaky one) and this makes it integral to the building of progressive web applications (fully optimised web applications that work offline like native applications).

Because it is impossible to predetermine your user-base at development time, it is important to build web services that can be accessed by a broad spectrum of users who may not have the best hardware or may have slow internet connections.

Progressive web applications were created to ensure that web services work on all devices. On mobile devices, they are designed to deliver a user experience that is close to that of native applications. Under the hood, PWAs use service workers to achieve the ideal behavior, and they leverage the Cache API for extra control over network resources.

This Google web fundamentals page describes service workers like this:

A service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don’t need a web page or user interaction. Today, they already include features like push notifications and background sync. In the future, service workers might support other things like periodic sync or geofencing. A core feature of a service worker is the ability to intercept and handle network requests, including programmatically managing a cache of responses.

We can see that caching can play an important role in the workflow of service workers. This article shows how the Cache API can be used in a service worker, and as a general mechanism of resource storage.

All the code in this tutorial can be found in this repository, feel free to fork it or send in a PR.

Detecting the Cache API

In modern browsers, each origin has a cache storage and we can inspect it by opening the browser developer tools:

  • On Chrome: Application > Cache > Cache Storage
  • On Firefox: Storage > Cache

Pro tip: In Chrome, you can visit chrome://inspect/#service-workers and click on the “inspect” option (directly under the origin of any already opened tab) to view logging statements for the actions of the service-worker.js script.

The Cache API is available in all modern browsers:

  • Edge >= 17
  • Opera >= 27
  • Safari >= 11.1
  • Firefox >= 39
  • Chrome >= 40
  • iOS Safari = 11.4 >
  • UC Browser 11.8 >=
  • Chrome for Android >= 67

Because older browsers may not support the API, it is good practice to check for its availability before attempting to reference it. The caches property is available on the window object and we can check that it is implemented in the browser with this snippet:

if ('caches' in window){
    // you can safely insert your snippet here
}

Usage

The Cache API is a great choice for caching URL-addressable resources, that is, you should use the Cache API when you work with network resources that are necessary to load your application. If your application deals with lots of data, you may cache the data that the user will most likely need on page load. These resources may include file-based content, assets, API responses, and web pages.

For the storage of significant amounts of structured data (including files/blobs), you should ideally use the IndexedDB API.

The Cache API ships with several methods to perform the following (CRUD) operations:

  1. Create a new cache
  2. Add (update) items to a cache
  3. Retrieve items from a cache
  4. Delete items from a cache

Let’s go over some ways to use these methods in our code.

Create a new cache

Before we can start storing request-response pairs into our cache storage, we need to create a cache instance. Each origin can have multiple cache objects within its cache storage. We can create a new cache object using the caches.open() method:

const newCache = await caches.open('new-cache');

The snippet above receives the name of the cache as the single parameter and goes on to create the cache with that name. The caches.open() method first checks if a cache with that name already exists. If it doesn’t, it creates it and returns a Promisethat resolves with the Cache object.

After the snippet executes, we will now have a new cache object that can be referenced with the name new-cache.

Adding items to a cache

There are three main ways to add items to the cache:

  1. add
  2. addAll
  3. put

All of these methods return a Promise, now let’s go over each of these and see how they differ from one another.

Cache.add()

The first method, cache.add(), takes a single parameter that can either be a URL string literal or a Request object. A call to thecache.add() method will make a Fetch request to the network and store the response in the associated cache object:

newCache.add('/cats.json')

or to gain more control, we can use a request object:

const options = {
    method: "GET",
    headers: new Headers({
        'Content-Type': 'text/html'
    }),
  }  
newCache.add(new Request('/cats.json', options))

Note: If the fetch is unsuccessful and an error response is returned, nothing is stored in the cache and the
Promise rejects.

Cache.addAll()

This method works similarly to the cache.add() method except that it takes in an array of request URL string literals or Requestobjects and returns a promise when all the resources have been cached:

const urls = ['pets/cats.json', 'pets/dogs.json'];
newCache.addAll(urls);

Note: The promise rejects if one or more items in the array of requests are not cached. Also, while the items in the array are being cached, a new entry overwrites any matching existing entry.

Cache.put()

The Cache.put method works quite differently from the rest as it allows an extra layer of control. The put() method takes two parameters, the first can either be a URL string literal or a Request object, the second is a Response either from the network or generated within your code:

// Retrieve cats.json and cache the response
newCache.put('./cats.json')

// Create a new entry for cats.json and store the generated response
newCache.put('/cats.json', new Response('{"james": "kitten", "daniel": "kitten"}'))

// Fetch a response from an external address and create a new entry for cats.json
newCache.put('https://pets/cats.json');

The put method allows an extra layer of control as it lets you store responses that do not depend on CORS or other responses that are dependent on a server response status code.

Pro tip: The first two methods — add() and addAll() — are dependent on the state of CORS on the server the data is being requested from. If a CORS check fails, nothing gets cached and the Promise rejects. Using put(), on the other hand, gives you extra confidence as you can set an in-house response.

Retrieving items from a cache

After we’ve added some items to the cache, we need to be able to retrieve them during run-time. We can use the match() method to retrieve our cached responses:

// retrieve a new response
const request = '/cats.json';
const response = await newCache.match(request);

In the code above, we passed in a request variable to the matchmethod, if the request variable is a URL string, it is converted to a Request object and used as an argument. The match method will return a Promise that resolves to a Response object if a matching entry is found.

The browser uses different factors in determining if two or more Requests match. A Request may have the same URL as another but use a different HTTP method. Two such requests are considered to be different by the browser.

When using the match method, we can also pass an options object as the second parameter. This object has key value pairs that tell match to ignore specific factors when matching a request:

// create an options object
const options = {
        ignoreVary: true, // ignore differences in Headers
        ignoreMethod: true, // ignore differences in HTTP methods
        ignoreSearch: true // ignore differences in query strings
    }

// then we pass it in here
const response = await newCache.match(request, options);

In a case where more than one cache item matches, the oldest one is returned. If we intend to retrieve all matching responses, we can use the matchAll() method.

Removing items from a cache

We might not need a cache entry anymore and want it deleted. We can delete a cache entry using the delete() method:

// delete a cache entry
const request = '/cats.json';
newCache.delete(request);

In the code above, we saved a URL string in the request variable but we can also pass in a Request object to the delete method. In a case where we have more than one matching entries, we can pass in a similar options Object as we did with the matchmethod.

Deleting a cache

Finally, we can delete a cache by calling the delete() method on the caches property of the window object. Let’s delete our cache in the snippet below:

// delete an existing cache
caches.delete('new-cache');

Note: When a cache is deleted, the delete() method returns a Promise if the cache was actually deleted and a false if something went wrong or the cache doesn’t exist.

Conclusion

In this article, we took a tour of the Cache API and discussed its usefulness to the development of progressive web applications. We also explored its CRUD methods and saw how easily we can retrieve responses and store requests.

Note: For security reasons, a cache is bound to the current origin and other origins cannot access the caches set up for other origins.

Comments

Popular posts from this blog

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 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...

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...