Skip to main content

IndexedDB vs LocalStorage: Your Guide to Storing Large Data and Smart Caching

IndexedDB vs LocalStorage: Your Guide to Storing Large Data and Smart Caching
javascript#javascript#indexeddb#localstorage#pwa+4 more

IndexedDB vs LocalStorage: Your Guide to Storing Large Data and Smart Caching

Hit the 5MB LocalStorage limit? Learn when to use IndexedDB for storing large datasets, offline support, and smart caching in modern web apps and PWAs.

Mohammad Alhabil

Author

December 28, 2024
6 min read
~1200 words

IndexedDB vs LocalStorage: Your Guide to Storing Large Data and Smart Caching

We've all tried to store large data in LocalStorage only to see this error:

QuotaExceededError: The quota has been exceeded.

True, LocalStorage is limited to about 5MB and designed to store small text strings. But when your project grows and you need to:

  • Store thousands of messages or API data for offline use
  • Save images or small files in a PWA
  • Use smart caching to speed up your site and reduce server load

That's when IndexedDB comes in!

The Critical Difference: Performance & Threading

FeatureLocalStorageIndexedDB
OperationSynchronousAsynchronous
ThreadBlocks main threadRuns in background
UI ImpactCan freeze UI with large dataNever blocks the UI

LocalStorage: Synchronous = Blocking

// ❌ This blocks the main thread!
localStorage.setItem('largeData', JSON.stringify(hugeArray));

// User can't interact with the page during this operation
const data = JSON.parse(localStorage.getItem('largeData'));

IndexedDB: Asynchronous = Non-Blocking

// ✅ This runs in the background
const transaction = db.transaction(['store'], 'readwrite');
const store = transaction.objectStore('store');
store.put(hugeArray, 'largeData');

// User can keep interacting with the page!

Quick Comparison

FeatureLocalStorageSessionStorageIndexedDB
Size Limit~5MB~5MB50MB+ (browser dependent)
Data TypesStrings onlyStrings onlyObjects, Blobs, Files, Arrays
PersistenceForeverUntil tab closesForever
API StyleSyncSyncAsync
IndexingNoNoYes (queryable)
TransactionsNoNoYes (ACID)

Real-World Scenarios for IndexedDB

1️⃣ API Caching for Faster Sites

Store product data locally with IndexedDB. The site displays it instantly without waiting for the server, while refreshing in the background:

import { openDB } from 'idb';

// Initialize database
const db = await openDB('myApp', 1, {
  upgrade(db) {
    db.createObjectStore('products', { keyPath: 'id' });
  },
});

// Cache-first strategy
async function getProducts(): Promise<Product[]> {
  // 1. Return cached data immediately
  const cached = await db.getAll('products');
  
  if (cached.length > 0) {
    // 2. Refresh in background (don't await)
    refreshProductsInBackground();
    return cached;
  }
  
  // 3. No cache? Fetch and store
  const fresh = await fetchFromAPI('/api/products');
  
  const tx = db.transaction('products', 'readwrite');
  fresh.forEach(product => tx.store.put(product));
  await tx.done;
  
  return fresh;
}

async function refreshProductsInBackground() {
  try {
    const fresh = await fetchFromAPI('/api/products');
    const tx = db.transaction('products', 'readwrite');
    await tx.store.clear();
    fresh.forEach(product => tx.store.put(product));
    await tx.done;
  } catch (error) {
    // Silently fail - we have cached data
  }
}

Benefits:

  • ⚡ Instant display (no loading spinner)
  • 📉 Less server pressure
  • 📴 Works offline

2️⃣ Chat App with Offline Support

If the internet disconnects, store messages locally and send them when connectivity returns:

import { openDB, DBSchema } from 'idb';

interface ChatDB extends DBSchema {
  messages: {
    key: string;
    value: {
      id: string;
      text: string;
      timestamp: number;
      status: 'pending' | 'sent' | 'failed';
    };
    indexes: { 'by-status': string };
  };
}

const db = await openDB<ChatDB>('chatApp', 1, {
  upgrade(db) {
    const store = db.createObjectStore('messages', { keyPath: 'id' });
    store.createIndex('by-status', 'status');
  },
});

// Send message (works offline!)
async function sendMessage(text: string) {
  const message = {
    id: crypto.randomUUID(),
    text,
    timestamp: Date.now(),
    status: 'pending' as const,
  };

  // Store locally first
  await db.put('messages', message);

  // Try to send
  if (navigator.onLine) {
    await syncMessage(message);
  }
  // If offline, will sync later
}

// Sync pending messages when online
async function syncPendingMessages() {
  const pending = await db.getAllFromIndex('messages', 'by-status', 'pending');
  
  for (const message of pending) {
    try {
      await fetch('/api/messages', {
        method: 'POST',
        body: JSON.stringify(message),
      });
      
      message.status = 'sent';
      await db.put('messages', message);
    } catch {
      message.status = 'failed';
      await db.put('messages', message);
    }
  }
}

// Listen for connectivity
window.addEventListener('online', syncPendingMessages);

3️⃣ Storing Files and Images in PWA

LocalStorage doesn't support Blobs or files, but IndexedDB stores any data type efficiently:

import { openDB } from 'idb';

const db = await openDB('mediaCache', 1, {
  upgrade(db) {
    db.createObjectStore('images', { keyPath: 'url' });
  },
});

// Cache image as Blob
async function cacheImage(url: string): Promise<string> {
  // Check cache first
  const cached = await db.get('images', url);
  if (cached) {
    return URL.createObjectURL(cached.blob);
  }

  // Fetch and cache
  const response = await fetch(url);
  const blob = await response.blob();
  
  await db.put('images', { url, blob, cachedAt: Date.now() });
  
  return URL.createObjectURL(blob);
}

// Usage in React
function CachedImage({ src, alt }: { src: string; alt: string }) {
  const [objectUrl, setObjectUrl] = useState<string>();

  useEffect(() => {
    cacheImage(src).then(setObjectUrl);
    
    return () => {
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    };
  }, [src]);

  if (!objectUrl) return <Skeleton />;
  return <img src={objectUrl} alt={alt} />;
}

Making IndexedDB Easier

The raw IndexedDB API is verbose and callback-based. Use these libraries to simplify:

idb (Lightweight Promise Wrapper)

npm install idb
import { openDB } from 'idb';

const db = await openDB('myDB', 1, {
  upgrade(db) {
    db.createObjectStore('users', { keyPath: 'id' });
  },
});

// Simple CRUD
await db.put('users', { id: 1, name: 'Mohammad' });
const user = await db.get('users', 1);
await db.delete('users', 1);
npm install dexie
import Dexie from 'dexie';

class MyDatabase extends Dexie {
  users!: Dexie.Table<User, number>;
  
  constructor() {
    super('myDB');
    this.version(1).stores({
      users: '++id, name, email',  // Auto-increment id, indexed fields
    });
  }
}

const db = new MyDatabase();

// Beautiful API
await db.users.add({ name: 'Mohammad', email: 'test@example.com' });
const users = await db.users.where('name').startsWith('Mo').toArray();

Best Practices

1. Design Your Schema Right

Create keys and indexes that make searching easy:

const db = await openDB('ecommerce', 1, {
  upgrade(db) {
    const store = db.createObjectStore('products', { keyPath: 'id' });
    
    // Create indexes for common queries
    store.createIndex('by-category', 'category');
    store.createIndex('by-price', 'price');
    store.createIndex('by-category-price', ['category', 'price']);
  },
});

// Now you can query efficiently
const electronics = await db.getAllFromIndex('products', 'by-category', 'electronics');

2. Batch Large Data Operations

Store large datasets in batches instead of all at once:

async function storeInBatches<T>(
  db: IDBPDatabase,
  storeName: string,
  items: T[],
  batchSize = 100
) {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const tx = db.transaction(storeName, 'readwrite');
    
    batch.forEach(item => tx.store.put(item));
    await tx.done;
    
    // Let UI breathe between batches
    await new Promise(r => setTimeout(r, 0));
  }
}

3. Monitor Storage and Clean Up

// Check available storage
async function checkStorageQuota() {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    const { usage, quota } = await navigator.storage.estimate();
    const percentUsed = ((usage! / quota!) * 100).toFixed(2);
    console.log(`Using ${percentUsed}% of available storage`);
    
    if (usage! / quota! > 0.8) {
      await cleanupOldCache();
    }
  }
}

// Clean old cached data
async function cleanupOldCache() {
  const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
  
  const tx = db.transaction('cache', 'readwrite');
  let cursor = await tx.store.openCursor();
  
  while (cursor) {
    if (cursor.value.cachedAt < oneWeekAgo) {
      await cursor.delete();
    }
    cursor = await cursor.continue();
  }
}

4. Handle Offline → Online Sync

// Queue operations when offline
const offlineQueue: Array<() => Promise<void>> = [];

async function saveWithSync(data: any) {
  // Always save locally
  await db.put('data', data);
  
  if (navigator.onLine) {
    await syncToServer(data);
  } else {
    offlineQueue.push(() => syncToServer(data));
  }
}

// Sync when back online
window.addEventListener('online', async () => {
  for (const operation of offlineQueue) {
    await operation();
  }
  offlineQueue.length = 0;
});

Decision Guide

Use CaseStorage Choice
User preferences, themeLocalStorage ✅
Auth tokens (short-lived)SessionStorage ✅
Large API responsesIndexedDB ✅
Offline data syncIndexedDB ✅
File/image cachingIndexedDB ✅
Simple key-value (< 1KB)LocalStorage ✅
Queryable dataIndexedDB ✅

Interfaces you love, code you understand. 💡

Topics covered

#javascript#indexeddb#localstorage#pwa#offline#caching#performance#web-storage

Found this article helpful?

Share it with your network and help others learn too!

Mohammad Alhabil

Written by Mohammad Alhabil

Frontend Developer & Software Engineer passionate about building beautiful and functional web experiences. I write about React, Next.js, and modern web development.