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


๐งฌ 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 usedput(key, value)
โ Adds new item or updates existing oneaddToFront(key, value)
โ Puts new item at the beginningmoveToFront(key)
โ Moves an item to the beginningremoveLast()
โ 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 frontput(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!
Subscribe to my newsletter
Read articles from Yash Grover directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
