Cache in JavaScript: Service Workers
Noura Boudiaf
Software Developer at Lohn & HR GmbH - PhD in Computer Science - Follow to learn about Code Quality, Java, Python, and JavaScript.
Caching is a technique for storing data in a temporary storage area called a cache. It aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. In the first article, I introduced the different possibilities for caching data in JavaScript and how to choose the appropriate caching possibility according to your needs. In a previous article, I introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. Then, I dedicated another article to using objects and data-* attributes to temporarily cache data in JavaScript. In this article, I will elucidate the use of service workers to cache static resources like HTML, CSS, and JavaScript files or API responses.
1. Service Workers API
This technique is used especially to cache static resources (HTML, CSS, etc.) or API responses. It helps enhance the responsiveness and performance of the web application. Please note that ‘service worker’ is a complex topic and it is impossible to cover all its features in this article. That’s why we focus only on the basic features to show how they cache resources and API responses.
The Service Worker API is a script that runs asynchronously and independently of the main browser thread.
- It is a kind of interface between the browser and the network.
- It is executed in the background.
- It handles tasks such as intercepting network requests and caching assets to allow the application to continue working even offline.
2. Tasks of Service Worker API
Let’s summarize the main tasks that service workers can perform when are ready to operate.
Interception of Network Requests: Service workers can intercept network requests and decide to handle them. Service workers can respond with cached resources or search data from the network according to some conditions like the network connection. If it is a poor connection, it responds to the client from cached resources, allowing fast loading and enhancing offline working.
Background Sync: Service workers can synchronize data in the background, sending data to the server even if the app is not open. For example, they save user input while the connection is unavailable and send the data when the connection is back.
Caching: Service workers cache resources such as HTML, CSS, JavaScript, and images. Thus the web app can be loaded faster and even continue working offline.
Push Notifications: Service workers handle push notifications so that the web app sends messages to users even if the corresponding web page is closed.
3. Security Considerations
- Since service workers intercept network requests to make decisions, they must operate in a secure environment, which means they can only operate over HTTPS.
- Service workers run in a thread independent of the main thread, so they do not block the normal execution of the user interface.
4. Benefits of Service Workers
- Service workers improve the performance of web applications. Since service workers cache resources, they significantly reduce server calls, which reduces load times and strain on the server.
- Worker services allow offline access. In other words, users can interact with the application without an internet connection.
- Through push notifications, service workers keep users informed with regular updates.
5. Service Worker Lifecycle
The Service Worker lifecycle is event-driven and consists of several phases that describe how a Service Worker is installed, activated, and managed. The following table summarizes the key phases of a Service Worker's lifecycle. Note that a step can only be executed if the previous step has been completed successfully.
6. Example of Registering a Service Worker
Let’s take an example of how you can cache resources and API responses using the service worker API. For this, we need some back-end code to return data. A straightforward way to do this is using Python’s Flask framework. Additionally, we develop HTML, CSS, and JavaScript code for the front-end.
Note. We only write the relevant part here to explain how to use service workers to cache resources and intercept network requests. If you want the full example, you can find it on GitHub here:
Python Code
In the back-end code, we first define ‘employees_datas’ as a list containing a collection of employees, where each dictionary represents data about an employee and includes details such as its ID, first name, last name, and email.
We define some routes to enable the user to access web pages. For instance, to access the root URL of the application, the index function is called, and it renders the ‘index.html’ template file to display the content on the web page.
We have also a route for ‘employee.html’ file and another one for ‘about.html’ file. Note that in the ‘employee’ function, the ‘render_template’ function renders the specified template file and passes the ‘employees_data’ variable to the template.
@app.route('/')
def index():
# Render the index.html file
return render_template('index.html')
@app.route('/employee')
def employee():
return render_template('employee.html', employees_data=employees_data)
HTML Code
In the front-end code, we define a few HTML files, ‘index.html’ as the main web page, ‘employee.html’ to display some content sent from the back-end, and ‘about.html’. An additional file called ‘layout.html’ will be defined as a common template that will be extended by the other files.
This HTML template provides the structure for a basic web page that uses the Bootstrap library for styling, jQuery for JavaScript functionality, and placeholders for dynamic content through a template engine ‘Jinja’ of the Flask framework.
The {% block swjs %}{% endblock %} block is a placeholder for a service worker script that can be inserted by the template engine only in the ‘index.html’ template.
Inside the ‘navbar’, there are different navigation links to different parts of the website:
- ‘Home’ links to ‘/’, which consists of loading the ‘index.html’ template.
- ‘Employee’ links to ‘/employee’, which consists of loading the ‘employee.html’ template.
- ‘About’ links to ‘/about’, which consists of loading the ‘about.html’ template.
<div class="container"> holds the page’s main content and uses Bootstrap’s container class for responsive layout.
{% block content %}{% endblock %} serves as a placeholder for page-specific content that other templates can insert.
<head>
...
{% block swjs %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-primary">
<a class="navbar-brand" href="/">SW Demo</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/employee">Employee</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about">About</a>
</li>
</ul>
</div>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
Within the ‘index.html’ file, we have the following code:
{% extends "layout.html" %}
{% block swjs %}
<script type="text/javascript" src="/static/js/main.js"></script>
{% endblock %}
{% block content %}
<h1>Welcome to the Home Page</h1>
<p>This is the home page of the Service Worker API Demo</p>
{% endblock %}
Let’s explain the main parts of this code:
{% extends "layout.html" %} indicates that this template file extends the ‘layout.html’ file, meaning it inherits the structure and styling defined in ‘layout.html’.
{% block swjs %} and {% endblock %} define a block named ‘swjs’ where you can insert content specific to JavaScript files. In this case, it includes a script tag that links to the ‘main.js’ file located in the /static/js/ directory.
{% block content %} and {% endblock %}: This defines a block named content where you can insert the main content of the page. In this case, there is an <h1> heading and a <p> paragraph welcoming users to the Home Page of the Service Worker API Demo.
The content of the employee template in the file ‘employee.html’ is suggested as follows:
{% extends "layout.html" %}
{% block content %}
<h1>Employee List</h1>
<p>This is the employee List</p>
<table>
<thead>
<tr>
<th>Firstname</th>
领英推荐
<th>Lastname</th>
<th>Email</th>
</tr>
</thead>
<tbody id="myTable">
{% for item in employees_data %}
<tr>
<td>{{ item.first_name }}</td>
<td>{{ item.last_name }}</td>
<td>{{ item.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
The above code is a Jinja template that extends a layout defined in ‘layout.html’. Within the {% block content %}, it creates an Employee List page with a table displaying employee data. The table has columns for Firstname, Lastname, and Email. It then loops through the ‘employees_data’ list and populates the table rows with the corresponding data for each employee’s first name, last name, and email address.
JavaScript Code
In the JavaScript code, we show how to register and use a service worker to cache data. Two JavaScript files are defined. The ‘main.js’ file is defined to register a service worker and the ‘sw.js’ file is used to encapsulate the service worker behavior.
In the ‘main.js’ file, the code waits for the document to be fully loaded. Once the document is ready, it calls the ‘registerServiceWorker()’ function.
$(document).ready(function(){
registerServiceWorker();
});
function registerServiceWorker() {
console.log('Register Service Worker ...')
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(href = 'sw.js')
.then(function(registration) {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(function(error) {
console.log('Service Worker registration failed:', error);
});
}
}
Function ‘registerServiceWorker()’: This function checks if the browser supports service workers by checking if ‘serviceWorker’ is in ‘navigator’. If service workers are supported, it attempts to register a service worker by calling ‘navigator.serviceWorker.register('sw.js')’. If the registration is successful, it logs a message to the console with the service worker’s scope. Note that the scope is the URL path that determines which pages the service worker will control. If the registration fails, it logs an error message to the console.
In the ‘sw.js’ file, we define the main behaviors of the service worker. This consists of handling the ‘install’, ‘activate’, and ‘fetch’ events. Thus we can install and activate the service workers. The ‘fetch’ event handler will intercept network requests to respond to the client from the cache.
First, we need a few global variables like the cache name (‘cache_name’) and the URLs we want to cache (‘urls_to_cache’).
// Cache name
const cache_name = 'sw-1';
// Urls to cache
const urls_to_cache = [
'/',
'/employee',
'/about',
'/static/css/styles.css',
'/static/js/main.js',
'/static/js/employee.js',
];
We add an event listener for the install event in the service worker as follows:
self.addEventListener('install', function(event) {
console.log('Installing ...');
event.waitUntil(
caches.open(cache_name).then(function(cache) {
console.log('Caching data ...');
return cache.addAll(urls_to_cache);
})
);
});
When the service worker is installed, it performs some actions. It uses ‘event.waitUntil()’ to extend the installation process until the provided promise is resolved successfully. This means the service worker will only be considered installed if everything within ‘waitUntil’ (in this case, the caching process) completes without errors.
It opens a cache (or creates one if it doesn’t exist) with the name specified by ‘cache_name’. Once the cache is opened, it adds all the URLs listed in the ‘urls_to_cache’ array to the cache for offline access. Note that the ‘Self’ word inside the service worker script refers to the Service Worker itself.
The activate event listener triggers when the service worker becomes active. This usually happens after installation and when there are no more active clients using the previous version of the service worker. The following code snippet is a listener for the ‘activate’ event:
// Activate event: Clean up old caches
self.addEventListener('activate', event => {
console.log('Activating ...');
const cacheList = [cache_name];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cName => {
if (!cacheList.includes(cName)) {
console.log('Deleting old cache:', cName);
return caches.delete(cName);
}
})
);
})
);
});
When a service worker is activated, it means it is ready to take control of the page and handle caching and other tasks. Let’s explain what the code does. This code defines an array ‘cacheList’ containing the value of ‘cache_name’. This array specifies the caches to keep. Only the caches in this array will be kept, the others will be deleted.
It uses ‘event.waitUntil(...)’ to delay the activation until all old caches are deleted. This ensures a clean start with only the necessary caches. Inside ‘event.waitUntil(...)’, it calls caches.keys() to get a list of all the cache names. It then uses ‘Promise.all(...)’ to map over each cache name and check if it's in the ‘cacheList’. If a cache name is not in the ‘cacheList’, it logs the cache to be deleted to the console and deletes that cache using ‘caches.delete(cName)’.
We now develop a common event listener that intercepts the ‘fetch’ event, which is triggered whenever a network request is made.
self.addEventListener("fetch", (event) => {
console.log('Fetching data ...');
// Intercept the network request to handle it
event.respondWith(
(async () => {
// Search the response first in a cache
let cache = await caches.open(cache_name);
let cachedResponse = await cache.match(event.request);
console.log("cachedResponse:", cachedResponse)
// Return a response if it is found in the cache
if (cachedResponse) return cachedResponse;
// If no response is already cached, use the network
return fetch(event.request);
})(),
);
});
After logging 'Fetching data ...' to the console, we call the ‘event.respondWith()’ method to intercept the network request and handle it. Within the ‘respondWith()’ method, we first try to find a response in a cache by opening a cache with the name ‘cache_name’ and attempting to match the request with the cached response. If a cached response is found, we return the cached response. If no cached response is found, we make a network request using fetch (‘event.request’).
7. Execution of the Example
When you run this example on the server, you get the following UI:
Visit all the pages and then stop the server. You will see that the application continues to work offline and the pages are showing normally.