Wednesday, October 16, 2024

Cache in JavaScript: In-memory Caching

 


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. I also introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. So, I will dedicate this article to using objects and data-* attributes to temporarily cache data in JavaScript.


1. In-memory Caching

This technique is used to retain data for a short period of time, such as the result of API calls during a single session. The data will be removed from the cache after the page refreshes. Even though it only caches data for short-term caching while a user is on a page, this technique is still an efficient way to use resources and retrieve data quickly, instead of repeatedly searching for it on the server.


In this section, we will see through examples two approaches to temporarily caching data:

- JavaScript objects or variables

- data-* attributes


1.1. Using Objects or Variables for In-Memory Caching

JavaScript objects or variables to store data in memory are mechanisms suitable for short-term or temporary caching while a user is on a page. Let’s take an example of how you can cache the result of an API call using objects. For this, we need a back-end code to return data. An easy way to do this is to use Python’s Flask framework. Moreover, we develop HTML, CSS, and JavaScript code for the front-end.


Note. We only write the relevant part here to explain how to use the objects to cache data. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/inmemory-caching-app-blog


Python Code

In the back-end code, we first define ‘james_bond_films’ as a list containing a collection of dictionaries, where each dictionary represents a James Bond film and includes details such as its ID, title, release year, star, and director.


james_bond_films = [

    {"id": 10, "title": "No Time to Die", "year": 2021, "starring": "Daniel Craig", "directedBy": "Cary Joji Fukunaga"},

    {"id": 20, "title": "Die Another Day", "year": 2002, "starring": "Pierce Brosnan", "directedBy": "Lee Tamahori"},

    {"id": 30, "title": "The Living Daylights", "year": 1987, "starring": "Timothy Dalton", "directedBy": "John Glen"},

    {"id": 40, "title": "A View to a Kill", "year": 1985, "starring": " Roger Moore", "directedBy": "John Glen"},

    {"id": 50, "title": "Never Say Never Again", "year": 1983, "starring": "Sean Connery", "directedBy": "Irvin Kershner"},

]


In this Python code, we define two methods that we explain as follows:


Method ‘get_film_data’: To return these details in response to an AJAX POST request, we define a Flask route in ‘/load_film_data’. It fetches the JSON data from the request, extracts the film ID, and searches for the corresponding film data in the predefined ‘james_bond_films’ list using the ‘search’ method. Finally, it returns the film data as a JSON response.


Method ‘search’: The search method takes two parameters, ‘id’ and ‘ls’. It searches for a film in the ‘ls’ list whose ID matches the provided ‘id’. If a match is found, it returns the corresponding data of that film as a dictionary. If no match is found, it returns None.


# Helper function to search for an item in a list

def search(id, ls):

    result = [element for element in ls if element["id"] == id]

    if len(result) > 0:

        return result[0]

    return None



# Route to handle AJAX POST request

@app.route('/load_film_data', methods=['POST'])

def get_film_data():

    # Get JSON data from request

    data = request.get_json()


    film_id = data.get('filmId')

    film_data = search(int(film_id), james_bond_films)

    print("Found film data: ", film_data)


    # Send a JSON response back to the client

    return jsonify({

        'status': 'success',

        'film_data': film_data

    })


HTML Code

In the front-end code, we define an HTML file with some elements like a drop-down list containing the list of some James Bond movies and a table to display the details of each movie when the user selects it.


<body>

    <div class="container">

        <h1>List of James Bond Films</h1>

        <select id="selectFilm" class="select-jb-film" onchange="onChangeJBFilm()" size="1">

            <option value="10">No Time to Die (2021)</option>

            <option value="20">Die Another Day (2002)</option>

            <option value="30">The Living Daylights (1987)</option>

            <option value="40">A View to a Kill (1985)</option>

            <option value="50">Never Say Never Again (1983)</option>

        </select>


        <br><br><br><br>

        <div id="film_detail">

            <table id="film_table">

                <thead>

                    <tr>

                        <th>Film Title</th>

                        <th>Year</th>

                        <th>Starring</th>

                        <th>Directed By</th>

                    </tr>

                </thead>

                <tbody>

                </tbody>

            </table>

        </div>

    </div>

</body>


JavaScript Code

In the JavaScript code, we implement the code for ‘onChangeJBFilm()’ and other behaviors to demonstrate the use of JavaScript objects to cache data. The code handles loading, storing, and displaying some data from the James Bond movie. In this section, we explain the main functions of the code.


We add a handler to the ‘DOMContentLoaded’ event to ensure that the JavaScript program starts once the HTML page is fully loaded and the elements are created. In this handler, we call the ‘onChangeJBFilm()’ function.


We define two global variables, the ‘cache’ object which we need to cache the data, and ‘apiUrl’ to get the data from the back-end.


// An object to store cached data

const cache = {};


// Url to get data from back-end

const apiUrl = "/load_film_data";


document.addEventListener('DOMContentLoaded', function() {

    onChangeJBFilm();

}, false);


Function ‘onChangeJBFilm()’: This function is called when the selected film is changed. This function is declared as ‘async’, meaning it will return a promise and can use the await keyword inside it. In the first line, it retrieves the current value of the drop-down, which is the selected movie’s ID.


The second statement line calls the ‘fetchFilmDataFromUrl()’ function with ‘apiUrl’ and ‘filmId’ as arguments. It waits for the function to complete and assigns the returned movie data to the ‘film_data’ variable. After logging the fetched film data into the console, the function called the ‘showMovieData()’ creates a new row in the ‘film_table’ table and displays the movie data.


async function onChangeJBFilm(){

    let filmId = document.getElementById("selectFilm").value;

    let film_data = await fetchFilmDataFromUrl(apiUrl, filmId);


    // Log the found data

    console.log("Film data: ", film_data);


    // Call the function with the URL and Id

    showMovieData(film_data);

}


Function ‘fetchFilmDataInMemCache()’: This function is defined to retrieve movie data from the cache. Otherwise, it fetches movie data from the back-end. The first statements of the function allow to retrieve the currently selected option from the select element. 


The function takes two arguments, ‘url’ as the endpoint where the data will be fetched from, and ‘filmId’, which is the ID of the specific film for which data is requested. The function creates a cache key by concatenating the ‘url’ and ‘filmId’. This uniquely identifies the cached data for that particular movie request.


The function checks if the data for the given ‘cacheKey’ is already stored in the cache. If so, the data will be returned immediately.


If the data is not cached, we need to make an API call. For that, the function creates a ‘dataToSend’ object containing the ‘filmId’ ID. It sends a POST request to the specified ‘url’ with ‘dataToSend’ as the request body and waits for a response. 


The function throws an error if the response is not OK. Otherwise, the response is parsed in JSON format. The retrieved data is stored in the ‘cache’ object using its ‘cacheKey’ key. Finally, the function returns the retrieved data.


// An async function that fetches data from a URL or in memory cache

async function fetchFilmDataInMemCache(url, filmId) {


    //Define cache key as the combination of url and the film ID

    let cacheKey = url+filmId;


    // Check if data is already in the cache to return it

    if (cache[cacheKey]) {

        console.log('Returning cached data');

        return cache[cacheKey];

    }


    console.log("Fetching data...");

    let dataToSend = {filmId: filmId}

    const response = await fetch(url,

        {

            method: 'POST',

            headers: {

                'Content-Type': 'application/json'

            },

            body: JSON.stringify(dataToSend)

        }

    );


    // Await the fetch request & throw an error if status is not ok

    if (!response.ok) {

        throw new Error(`HTTP error! status: ${response.status}`);

    }


    // Await the parsing of the response as JSON

    const data = await response.json();


    // Store the fetched data in cache

    cache[cacheKey] = data.film_data;

    return data.film_data;

}


Function ‘showMovieData(film_data)’: This function dynamically creates a row in the movie details table with data such as the movie title, release year, lead role, and director. Let’s note that we create only one row every time we insert movie data. So, we clear the table’s body before adding the new row. 


function showMovieData(film_data){

    let table = document.getElementById("film_table");

    let tBody = table.getElementsByTagName('tbody')[0];

    // Clear the tbody contents to create a new row

    tBody.innerHTML = "";

    let film_title = film_data["title"];


    //Add a new row

    let tr = document.createElement('tr');

    addCell(tr, film_title);

    addCell(tr, film_data["year"]);

    addCell(tr, film_data["starring"]);

    addCell(tr, film_data["directedBy"]);

    tBody.appendChild(tr);

}


Function ‘addCell(tr, item)’: This function adds a cell with a given content to a table row.


function addCell(tr, item){

    var td = document.createElement('td');

    var content = document.createTextNode(item);

    td.appendChild(content);

    tr.appendChild(td);

}


If you execute this code and open the browser, you find the following page:



1.2. Using Element Data Attributes for In-Memory Caching

In JavaScript, you can use data-* attributes to cache data on DOM elements. The role of data-* attributes is to store any additional information you need it in your code. The content of data-* attributes must be valid HTML5.

You can add data-* attributes to HTML elements directly, or you can add and access them in JavaScript. We briefly discuss the two mechanisms for using the defined data-* attributes. We then present an example of caching data using data-* attributes in JavaScript.


Adding data-* attributes to HTML elements

In your HTML, you can add use data-* attribute like that:

<div id="myFilm" data-film-id="10" data-film-title="Die another day"></div>


Accessing data-* attributes in JavaScript

In JavaScript, you can access data-* attributes using the dataset property of the DOM element. The dataset property is a read-write object that contains all the data-* attributes of an element.


// Select the element

let element = document.getElementById('myFilm');


// Read data attributes using the dataset property

let filmId = element.dataset.filmId;


// Write data in the element's data attributes

element.dataset.filmId = "Never Say Never Again";


We can also cache JavaScript objects using data-* attributes, but we must to convert them first to a string.


Example of Using Attribute Data of an Element

We keep the same example above, except we remove the ‘fetchFilmDataInMemCache()’ function and define a new function called ‘fetchFilmDataElemCache()’ to cache the data using the data-* attribute.


Function ‘fetchFilmDataInMemCache()’: This function is defined as an asynchronous function that takes a ‘url’ and a ‘filmId’ as parameters. In the first two statements, it selects an HTML element using its ‘selectFilm’ ID and retrieves the currently selected option from this element.

It checks if the selected option already has movie data stored in its data-* attribute. If so, it logs a message and returns the cached data. Here, we use ‘JSON.parse’ to convert the cached data, which is a string, into a JavaScript object.

If the data is not cached, it sends a POST request to the specified URL with the ‘filmId’ in the request body. It then waits for the response, checks for errors parses the response as JSON, and stores the fetched data in the option’s data-* attribute for future use. Here we need to convert the ‘data.film_data’ object to a string that we can store in the data-* attribute.


// An async function that fetches data from a URL or an element cache

async function fetchFilmDataElemCache(url, filmId) {


    // Select the element

    let element = document.getElementById('selectFilm');


    //Get the selected option

    var option = element.options[element.selectedIndex];


    // Access data attributes using the dataset property

    let filmData = option.dataset.filmData;


    // Check if data is already in cache to return it

    if (filmData) {

        console.log('Returning cached data');

        return JSON.parse(filmData);

    }


    console.log("Fetching data...");

    let dataToSend = {filmId: filmId}

    const response = await fetch(url,

        {

            method: 'POST',

            headers: {

                'Content-Type': 'application/json'

            },

            body: JSON.stringify(dataToSend)

        }

    );


    // Await the fetch request & throw an error if status is not ok

    if (!response.ok) {

        throw new Error(`HTTP error! status: ${response.status}`);

    }


    // Await the parsing of the response as JSON

    const data = await response.json();


    // Store the fetched data in the cache

    option.dataset.filmData = JSON.stringify(data.film_data);

    return data.film_data;

}




Saturday, October 5, 2024

Cache in JavaScript



Caching is a technique used to store data in a temporary storage area called a cache. Caching aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. Thus, caching helps improve the performance of multiple applications, including web applications. Caching helps decrease the time required to access data. JavaScript offers various possibilities for performing data caching or resources for a short or long time. In this article, I want to present how you can cache your data or resources in JavaScript. I explain how to choose the appropriate caching possibility according to your needs.


1. Common Caching Techniques in JavaScript

JavaScript offers a variety of caching techniques, each suited to specific situations. This may depend on the volume of data you want to cache, the nature of the data, and how long you need to persist your data. Here are the most common techniques to cache data in JavaScript.


- Web Storage API (localStorage and sessionStorage): This technique is the best fit to cache small pieces of data like default or user preferences values for search inputs or selected items for drop-down lists. Web Storage API is a technique to store data using the concept of key-value pair. It includes two types of storage,  localStorage and sessionStorage. LocalStorage is used to cache data permanently. However, sessionStorage allows data to be stored only for a session until the browser is closed.


- In-memory JavaScript Objects: It is used to persist data for a short time, like the result of API calls in a single session. The data will be deleted from the cache after the page refreshes. For example, if you have a dropdown list and every time you select an item (employee), you make an Ajax call to retrieve their data (birth date, email, etc.) from the backend. It will be better to cache this data. Every time you come back in the same single session to select an employee you have already selected, your code will fetch the data from the cache. It is more efficient, faster, and without loading the server.


- Service Workers: 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.


- IndexedDB: It is a low-level API to store large amounts of data, which is good for complex applications that need to cache large JSON responses or images. Thus, it allows working offline.


In this article, we through in detail the first technique and leave the others for other articles.


2. Comparison between Caching Methods

Here is a comparison table between the four above techniques. We consider in our comparison the main criteria such as data persistence time, storage capacity, use cases, the type of data that can be cached, and performance.

Technique

Web Storage API (localStorage/sessionStorag)

In-memory JavaScript Objects

Service Workers Cache API

IndexedDB

Data Persistence

- Persistent for localStorage

- Temporary for sessionStorage (only for a session)

Temporary (only when page is active)

Persistent (even after closing the page)

Persistent (even after closing the page)

Storage Capacity




- Limited and depends on the browser

- Usually 5-10 MB

Limited to available memory

- Depends on the browser

- Commonly larger than Web Storage

- Virtually unlimited

- Limits set by the browser

Data Structure Support


- String only

- Objects need manual serialization to be cached

Any data structure

Requests and responses (for assets like HTML, CSS, JS, etc.)

Complex data structures

Use Case

Storing small amounts of data like user preferences or settings

Caching data like the result of API calls for fast access in a single page load

Caching static elements like HTML, CSS, JS for offline use or to speed up loading

Storing large datasets like a client-side database,

large JSON responses, or images

Performance

Fast read/write for small data

Fastest data reading/writing technique because data is stored in memory

Slower than in-memory

Slower than in-memory or localStorage because it caches a large amount of data


3. Web Storage API (LocalStorage and SessionStorage)

LocalStorage and sessionStorage are used to cache a small amount of data. They store data in the form of key-value pairs. SessionStorage keeps data only for the current session. In other words, it saves data until you close the browser or tab. However, localStorage keeps data for a long time, even if you close the browser.


3.1. Main Properties and Methods

In the table below you will find the principal localStorage properties and methods you need to know to cache data. Note that sessionStorage has the same properties and methods.


LocalStorage Properties

Property

Definition

Syntax

length

Returns the count of the key-value pairs in the localStorage.

localStorage.length

LocalStorage Methods

Method

Definition

Syntax

clear

Removes all key-value pairs in storage.

localStorage.clear()

getItem

- Retrieve the value corresponding to the key.

- If the key does not exist, it returns null.

- If the cached value is a serialized object, you can convert it from a string to a JavaScript object using JSON.parse.

localStorage.getItem(‘key’)

setItem

- Stores a key-value pair.

- key and value are stored as strings.

- to store an object, you should convert it to a string using JSON.stringify.

localStorage.setItem(‘key’, value)

removeItem

Removes the key-value pair from the storage.

localStorage.removeItem(‘key’)

key

- Returns the name of the key at a specific index in the storage.

- localStorage saves key-value pairs in the order they were inserted.

localStorage.key(keyindex)



3.2. Example of Caching Data Using LocalStorage

Let’s take an example to show how to use localStorage to cache data. We are developing a small web application with Python's Flask framework for the backend and HTML, CSS, and JavaScript for the frontend.


Note. We only write the relevant part here to explain how to use the Web Storage API to cache data. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/webstorage-app-blog


Python Code

In the backend code, we first define ‘countries’ as a list containing a collection of dictionaries, where each dictionary represents a country and includes details such as its ID, name, capital, population, area, and dialing code.


countries = [

    {'Id': 10, 'Name': 'Austria', 'Capital': 'Vienna', 'Population': '9M', 'Area': '83,879 km2', 'Dialing_code': '+43'},

    {'Id': 20, 'Name': 'Germany', 'Capital': 'Berlin', 'Population': '84M', 'Area': '357,592 km2',

     'Dialing_code': '+49'},

    {'Id': 30, 'Name': 'France', 'Capital': 'Paris', 'Population': '67M', 'Area': '551,695 km2', 'Dialing_code': '+33'},

    {'Id': 40, 'Name': 'Italy', 'Capital': 'Rome', 'Population': '58M', 'Area': '301,230 km2', 'Dialing_code': '+39'},

    {'Id': 50, 'Name': 'Spain', 'Capital': 'Madrid', 'Population': '47M', 'Area': '505,990 km2', 'Dialing_code': '+34'},

    {'Id': 60, 'Name': 'England', 'Capital': 'London', 'Population': '56M', 'Area': '130,279 km2',

     'Dialing_code': '+44'},

]


In this Python code, we define two methods that we explain as follows:


Method ‘load_country_detail’: To return these details in response to an AJAX POST request, we define a Flask route in /load_country_detail. It fetches the JSON data from the request, extracts the country ID, and searches for the corresponding country details in the predefined list of countries using the ‘search’ method. Finally, it returns the country details as a JSON response.


Method ‘search’: The search method takes two parameters, ‘ct_id’ and ‘ls’. It searches for a country in the ‘ls’ list whose ‘id’ matches the provided ‘ct_id’. If a match is found, it returns the details of that country as a dictionary. If no match is found, it returns None.


@app.route('/load_country_detail', methods=['POST'])

def load_country_detail():

    # Get JSON data from request

    data = request.get_json()


    country_id = data.get('countryId')

    country_detail = search(int(country_id), countries)

    print("Found country detail: ", country_detail)


    # Send a JSON response back to the client

    return jsonify({

        'status': 'success',

        'country_detail': country_detail

    })


def search(ct_id, ls):

    result = [element for element in ls if element['Id'] == ct_id]

    if len(result) > 0:

        return result[0]

    return None


HTML Code

In the frontend code, we define an HTML file with some elements like a dropdown list containing the list of some European countries and a table to display the details of each country when the user selects it.


<body>

    <div class="container">

        <h1>List of European Countries</h1>

        <select id="selectCountry" class="select-country" onchange="onChangeCountry()" size="1">

            <option value="10">Austria</option>

            <option value="20">Germany</option>

            <option value="30">France</option>

            <option value="40">Italy</option>

            <option value="50">Spain</option>

            <option value="60">England</option>

        </select>


        <br><br><br><br>

        <div id="country_detail">

            <table id="country_table">

                <thead>

                    <tr>

                        <th>Country Name</th>

                        <th>Capital</th>

                        <th>Population</th>

                        <th>Area</th>

                        <th>Dialing Code</th>

                    </tr>

                </thead>

                <tbody>

                </tbody>

            </table>

        </div>

    </div>

</body>


JavaScript Code

In the JavaScript code, we implement the code of the ‘onChangeCountry()’ and other behaviors to demonstrate the usage of localStorage as a data caching mechanism. In summary, the code handles the loading, storing, and displaying country details. Here's a breakdown of the key functions.


We add a handler to the ‘DOMContentLoaded’ event to ensure that the JavaScript program starts once the HTML page is completely loaded and the elements are created. Within this handler, we call the ‘loadCountry()’ function.


document.addEventListener('DOMContentLoaded', function() {

   loadCountry();

}, false);


Function ‘loadCountry()’: This function retrieves the country name value from the browser’s localStorage, where the data can be stored persistently even after a page reload. If the element does not exist, the ‘storedCountry’ will be null.

If ‘storedCountry’ is not null, the function sets the drop-down value to the stored value. This ensures that when the page reloads, the drop-down will display the previously selected country.


function loadCountry() {

  let storedCountry = localStorage.getItem("country");

  console.log("Value of country in localStorage is:", storedCountry)

  console.log(document.getElementById("selectCountry").value)

  if (storedCountry){

     document.getElementById("selectCountry").value = storedCountry;

  } else {

     localStorage.setItem("country", document.getElementById("selectCountry").value);

  }

  onChangeCountry();

}


Function ‘onChangeCountry()’: This function is called when the selected country is changed. It updates the localStorage with the value of the selected country. Here we use the string ‘country’ as a key to save the country ID in the localStorage.

Then the function fetches the data for the selected country from localStorage using the country ID. If the data is found, we need to convert it to an object using the built-in JavaScript function ‘JSON.parse()’. This function takes a JSON string and converts it into a JavaScript object. If the country data is not found, the ‘getCountryDetail()’ function will be called.


function onChangeCountry(){

    let selValue = document.getElementById("selectCountry").value;

    console.log("Store the value:", selValue, " in localStorage")

    localStorage.setItem("country", document.getElementById("selectCountry").value);


    let id = document.getElementById("selectCountry").value;

    let country_detail = localStorage.getItem(id);

    console.log("country_detail:", country_detail);

    if (country_detail){

        showCountryDetail(JSON.parse(country_detail));

    } else {

        getCountryDetail(document.getElementById("selectCountry").value);

    }

}


Function ‘getCountryDetail(countryId)’: This function sends a POST request to '/load_country_detail' endpoint with the selected country ID, retrieves the country details, stores them in localStorage, and updates the country details table. To cache an object in localStorage, we use the country ID as a key, and then we need to serialize the object using the built-in function ‘JSON.stringify()’ which converts the object to a string.


function getCountryDetail(countryId) {

    const dataToSend = {

        countryId: countryId

    };


    // Sending a POST request using fetch

    fetch('/load_country_detail', {

        method: 'POST',

        headers: {

            'Content-Type': 'application/json'

        },

        body: JSON.stringify(dataToSend)

    })

    .then(response => response.json())

    .then(data => {

        console.log('Success:', data);

        localStorage.setItem(data.country_detail["Id"], JSON.stringify(data.country_detail));

        showCountryDetail(data.country_detail);

    })

    .catch((error) => {

        console.error('Error:', error);

    });

}


Function ‘showCountryDetail(country_detail)’: This function dynamically creates a row in the country details table with information such as country name, capital, population, area, and dialing code. Let’s note that we create only one row every time we insert country data. So, we clear the table’s body before adding the new row.


Function ‘findRow(tBody, country_name)’: This function checks if a row with the given country name already exists in the table to avoid duplicates.


Function ‘addCell(tr, item)’: This function adds a cell with a given content to a table row.


When you execute this code and you opened the browser, you find the following interface:




4. Web Storage API in Chrome DevTools

Open Chrome DevTools and go to the Application tab. Here you can manage localStorage and sessionStorage. You can clear them. You can also edit, delete any key-value pair, or look at what is inside. This is very useful for debugging your web application.








Blog Posts

Enhancing Performance of Java-Web Applications

Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and...