Build and Optimize an LRU Cache in JavaScript โ€” The Simple Way

Yash GroverYash Grover
6 min read

๐Ÿงฌ 1. What is an LRU Cache?

LRU Cache stands for "Least Recently Used" cache. Think of it as a smart storage box that remembers what you used last and throws away the oldest stuff when it gets full.

๐Ÿ“ฆ Easy example:

Your phone only shows your last 5 recent calls. When you make a 6th call, it removes the oldest one from the list to make room for the new one.

๐Ÿ’ป Why use it in web development?

  • Save user data when they switch between tabs

  • Remember API responses so you don't fetch them again

  • Store images or files temporarily

  • Keep React component data between page visits


๐Ÿ”ง 2. Building a Simple LRU Cache (with React)

Let's build a tab system that remembers the last 3 tabs you opened. Instead of loading data every time, it saves the data in memory.

๐Ÿ”— App.js

import "./styles.css";
import DynamicContentLoader from "./components/DynamicContentLoader";

export default function App() {
  return (
    <div className="App">
      <DynamicContentLoader />
    </div>
  );
}

โš›๏ธ DynamicContentLoader.js

import { useState } from "react";
import useLRUCache from "../hooks/useLRUCache.js";

const DynamicContentLoader = () => {
  const [content, setContent] = useState([]);
  const { get, put } = useLRUCache(3);

  const loadTabContent = async (id) => {
    await new Promise((resolve) =>
      setTimeout(() => {
        console.log("Loaded API content with id", id);
        const loadedContent = {
          id: id,
          content: `Tab ${id} data`,
        };
        put(id, loadedContent);
        setContent((prev) => [...prev, loadedContent]);
      }, 1000)
    );
  };

  const handleButtonClick = (id) => {
    const cachedContent = get(id);
    if (cachedContent) {
      setContent((prev) => [...prev, cachedContent]);
      console.log("Loaded cached content with id", id);
    } else {
      loadTabContent(id);
    }
  };

  return (
    <div>
      <h1>Dynamic Content Loader</h1>
      <button onClick={() => handleButtonClick(1)}>Tab 1</button>
      <button onClick={() => handleButtonClick(2)}>Tab 2</button>
      <button onClick={() => handleButtonClick(3)}>Tab 3</button>
      <button onClick={() => handleButtonClick(4)}>Tab 4</button>
      <button onClick={() => handleButtonClick(5)}>Tab 5</button>
      <ul>
        Following is the data :
        {content.map((item, i) => (
          <li key={(item.id, i)}>{item.content}</li>
        ))}
      </ul>
    </div>
  );
};

export default DynamicContentLoader;

๐Ÿ“š useLRUCache.js (Simple Version)

import { useRef } from "react";

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = {};
    this.head = null;
    this.tail = null;
  }

  get(key) {
    // if we have the key inside cache
    if (this.cache[key]) {
      // move to the front as it's the MRU
      this.moveToFront(key);
      // return the value
      return this.cache[key].value;
    }
    // if there's no key return null
    return null;
  }

  put(key, value) {
    // if the key already exists
    if (this.cache[key]) {
      // update the value (this basically means Keep the existing node, but update its value property.)
      this.cache[key].value = value;
      // move it to front (this.moveToFront(key))
      this.moveToFront(key);
    } else {
      // now if we are here that means that value doesn't exists
      // so we will check if there's space in LRUCache to store the new value else we will make some space
      if (Object.keys(this.cache).length === this.capacity) {
        // this basically means there is no space
        // removeLast()
        this.removeLast();
      }
      // now we basically have/made space to addToFront
      this.addToFront(key, value);
    }
  }

  addToFront(key, value) {
    const newNode = { key, value, next: null };
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.next = this.head;
      this.head = newNode;
    }
    // this basically means Replace the node entirely with just the raw value
    this.cache[key] = newNode;
  }

  moveToFront(key) {
    // sbse pehle take out the current node
    let current = this.cache[key];

    // now check if current is already at front, cuz then we will need to do nothing
    if (this.head === current) return;

    // else take a prev and present node and traverse to get to the current node
    let prev = null;
    let node = this.head;
    // traversing as long as the list is there and as long as we dont reach the current node
    while (node && node.key !== key) {
      prev = node;
      node = node.next;
    }
    // if we find no node like the one we need simply return
    if (!node) return;

    // if our node is the last node assign the tail to prev.
    if (node === this.tail) {
      this.tail = prev;
    }
    // if our node is somewhere in the middle assign the prev. node's next to next of our node
    if (prev) {
      prev.next = node.next;
    }
    // finally assign our node to head
    node.next = this.head;
    this.head = node;
  }

  removeLast() {
    // agrr head hee nhi h toh return
    if (!this.head) return;

    // if hai kuch linkedlist m toh go to tail and delete it
    const lastKey = this.tail.key;
    delete this.cache[lastKey];

    // now simply set the head and tail to right positions
    if (this.head === this.tail) {
      this.head = null;
      this.tail = null;
    } else {
      let current = this.head;
      while (current.next !== this.tail) {
        current = current.next;
      }

      current.next = null;
      this.tail = current;
    }
  }
}

const useLRUCache = (capacity) => {
  const cacheRef = useRef(new LRUCache(capacity));
  const get = (key) => cacheRef.current.get(key);
  const put = (key, value) => cacheRef.current.put(key, value);
  return { get, put };
};

export default useLRUCache;

โœ… What each function does:

  • get(key) โ€” Gets an item and marks it as recently used

  • put(key, value) โ€” Adds new item or updates existing one

  • addToFront(key, value) โ€” Puts new item at the beginning

  • moveToFront(key) โ€” Moves an item to the beginning

  • removeLast() โ€” Removes the oldest item


๐Ÿš€ 3. Making it Faster with Better Code ( using doubly linked list )

The first version works but can be slow. Here's a faster version that can move items around much quicker using a doubly linked list.

๐Ÿ” Faster Version of useLRUCache.js

import { useRef } from "react";

class Node {
  constructor(key, value) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = {};
    this.head = new Node(null, null);
    this.tail = new Node(null, null);
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  get(key) {
    if (!this.cache[key]) return null;
    const node = this.cache[key];
    this._remove(node);
    this._add(node);
    return node.value;
  }

  put(key, value) {
    if (this.cache[key]) {
      const node = this.cache[key];
      node.value = value;
      this._remove(node);
      this._add(node);
    } else {
      if (Object.keys(this.cache).length >= this.capacity) {
        const lru = this.tail.prev;
        this._remove(lru);
        delete this.cache[lru.key];
      }
      const newNode = new Node(key, value);
      this.cache[key] = newNode;
      this._add(newNode);
    }
  }

  _remove(node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
  }

  _add(node) {
    node.next = this.head.next;
    node.prev = this.head;
    this.head.next.prev = node;
    this.head.next = node;
  }
}

const useLRUCache = (capacity) => {
  const cacheRef = useRef(new LRUCache(capacity));
  const get = (key) => cacheRef.current.get(key);
  const put = (key, value) => cacheRef.current.put(key, value);
  return { get, put };
};

export default useLRUCache;

โœ… What each function does:

  • get(key) โ€” Gets an item and moves it to the front

  • put(key, value) โ€” Adds or updates an item and moves it to the front

  • _add(node) โ€” Puts an item right after the beginning

  • _remove(node) โ€” Removes an item super quickly


โœ… Key Takeaways

  • LRU Caches help make your website faster by remembering recently used data

  • Start with the simple version, then upgrade to the faster one if needed

  • This works in React, regular JavaScript, or even backend code

  • Perfect for saving API calls, user data, and improving user experience

The cache automatically manages memory for you, so you don't have to worry about running out of space!

0
Subscribe to my newsletter

Read articles from Yash Grover directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yash Grover
Yash Grover