Chapter 49: IndexedDB Client-Side Database

Building an Offline Cache with IndexedDB

Persisting media Blobs and binary files, constructing resilient offline mutation outbox queues, and syncing with Service Workers.

LEARNING OBJECTIVES
  • Persist large binary media assets (Blob, File, ArrayBuffer) natively inside IndexedDB.
  • Render cached binary assets in the DOM using URL.createObjectURL() and memory-safe URL.revokeObjectURL().
  • Construct an offline Mutation Outbox Queue to capture user actions while disconnected from the network.
  • Implement an automatic synchronization pipeline triggered by window.addEventListener('online') and Service Worker Background Sync.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine an explorer trekking through the Sahara Desert with a field tablet.

If the application relies solely on live network APIs, the moment the explorer loses cell reception, the screen freezes with a "Network Error" alert. Any research notes entered into the app vanish into the void.

An Offline-First IndexedDB Architecture acts like a physical field notebook and courier pouch:

  1. The Field Album (Blob Storage): High-resolution satellite maps and camera photos are downloaded ahead of time and stored as binary Blob objects directly inside IndexedDB. The explorer can zoom into terrain maps offline with zero network latency.
  2. The Outbox Pouch (Mutation Queue): When the explorer records a new rock sample, the app immediately writes the record to an outbox ObjectStore and optimistically renders it on screen.
  3. The Courier Handshake (Sync Pipeline): The app listens for connectivity (navigator.onLine). The instant the explorer reaches an oasis with Wi-Fi, the sync worker awakens, processes each pending outbox envelope in order, posts them to the server, and purges the queue upon server confirmation.

Technical Deep Dive & Specifications

Storing Binary Blobs vs Base64 Strings

A common anti-pattern in early web development was converting images to Base64 strings to store in localStorage.

+-----------------------------------------------------------------------------+
|                     BASE64 vs NATIVE BLOB STORAGE COMPARISON                |
+---------------------+-------------------------+-----------------------------+
| Metric / Property   | Base64 in localStorage  | Native Blob in IndexedDB    |
+---------------------+-------------------------+-----------------------------+
| Storage Overhead    | ~33% bloat (text enc)   | 0% bloat (raw binary bytes) |
| Memory Allocation   | Multi-MB string alloc   | Direct pointer / buffer ref |
| Thread Blocking     | Blocks main UI thread   | Asynchronous background I/O |
| Quota Limit         | 5 MB total quota        | Gigabytes / Multi-hundred MB|
| DOM Rendering       | Memory-heavy data URI   | `URL.createObjectURL(blob)` |
+---------------------+-------------------------+-----------------------------+

The Offline Mutation Queue Architecture

                                  USER ACTION
                         (e.g., "Post New Comment")
                                      │
                                      ▼
                        ┌───────────────────────────┐
                        │   Write to IndexedDB:     │
                        │   ObjectStore: `outbox`   │
                        └───────────────────────────┘
                                      │
                         Optimistic UI Update (Instant)
                                      │
                                      ▼
                            [Is navigator.onLine?]
                                 /          \
                          ONLINE/            \ OFFLINE
                               v              v
               ┌───────────────────────┐   [Wait for 'online' event]
               │ Process Outbox Queue  │              │
               │ POST /api/v1/comments │ <────────────┘
               └───────────────────────┘
                               │
                       [Server Returns 200 OK?]
                               │
                               ▼
               ┌───────────────────────┐
               │ Delete from `outbox`  │
               └───────────────────────┘

💻 Interactive Code Playground

Starter Code

Save this file as offline-sync.html and open it in your browser.

Line-by-Line Code Breakdown

  • Lines 104–112 (const blob = new Blob([...]); tx.objectStore('media_cache').put({ id, blob })): Demonstrates native binary Blob storage. IndexedDB writes the raw binary stream directly into browser storage without base64 text expansion.
  • Lines 123–128 (URL.createObjectURL(blob)): Creates a lightweight in-memory DOM reference string (e.g. blob:http://...) pointing directly to the cached image.
  • Line 128 (img.onload = () => URL.revokeObjectURL(objectUrl)): Essential memory management practice. Once the browser decodes the image into the GPU canvas, revoking the object URL frees the blob pointer.
  • Lines 135–148 (tx.objectStore('outbox').add(mutation)): Captures user edits into an outbox queue immediately, guaranteeing zero data loss even if the device powers off or loses connectivity.
  • Lines 153–170 (processOutbox()): Iterates through pending queue items, dispatches them sequentially to the backend API, and deletes each item from the outbox only after receiving a successful HTTP response.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
[03:10:00] 🛠️ Created "media_cache" and "outbox" ObjectStores.
[03:10:00] ✅ OfflineAppDB ready.
[03:10:05] 💾 High-resolution binary Blob saved to IndexedDB!
[03:10:08] 🖼️ Loaded Blob from IndexedDB (273 bytes) via URL.createObjectURL()!
[03:10:12] 📡 Disconnected from network. Mutations will queue locally.
[03:10:15] 📝 Note saved locally in "outbox": "Desert sample #402 collected"
[03:10:20] 🌐 Connection restored! Initiating outbox sync...
[03:10:20] 🚀 Syncing 1 pending mutation(s) with remote server...
[03:10:21]   ✅ Synced [ID: 1] "Desert sample #402 collected" ➔ Server 200 OK

🏋️ Hands-On Exercise

🎯 The Challenge: Persistent Offline Audio Player Cache

Instructions:

  1. Open database AudioCacheDB with store audio_tracks (keyPath: "trackId").
  2. Create an async function cacheAudioTrack(trackId, title, audioBlob) that stores the audio blob.
  3. Create a function playCachedTrack(trackId):
    • Retrieves the track from IndexedDB.
    • Creates an object URL via URL.createObjectURL(track.blob).
    • Injects the URL into an HTML5 <audio controls> element.
  4. Test by creating a sample audio tone Blob and verifying playback.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Leaking Memory with URL.createObjectURL: Forgetting to call URL.revokeObjectURL() after rendering images or audio causes the browser process to retain the underlying binary blobs in memory for the lifetime of the document.
  2. Deleting Outbox Items Before Server Confirmation: Deleting an outbox record before the HTTP POST returns 200 OK will result in silent permanent data loss if the network fails midway.
  3. Not Handling Idempotency on Sync: If the device loses connection right after the server receives a request but before the response reaches the client, the client will retry. Ensure mutations include unique clientMutationId UUIDs so the backend can deduplicate requests.

💡 Pro Tips

  1. Coordinate with Service Worker Background Sync: In Progressive Web Apps, register a sync event (navigator.serviceWorker.ready.then(reg => reg.sync.register('sync-outbox'))). The browser will wake up your Service Worker and flush the IndexedDB outbox even if the user has closed the tab!
  2. Use IndexedDB for Media, Cache Storage for Requests: Store structured documents, state queues, and dynamically generated Blobs in IndexedDB; store static network assets (CSS, JS bundles, HTML pages) in the Cache Storage API.

📌 Key Takeaways

  • IndexedDB stores binary Blob, File, and ArrayBuffer objects natively with 0% Base64 encoding overhead.
  • Cached media Blobs are rendered in the DOM using URL.createObjectURL(blob) and cleaned up with URL.revokeObjectURL(url).
  • An Outbox Mutation Queue enables offline-first interactions, capturing user intent locally and replaying operations once connected.
  • The window.addEventListener('online') hook provides automatic queue flushing when connectivity returns.
  • Client-generated UUIDs ensure backend idempotency during offline retry synchronizations.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why is storing raw Blob objects in IndexedDB superior to storing Base64 strings in localStorage?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What is the critical reason to call URL.revokeObjectURL(url) after setting an image source from an IndexedDB Blob?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

In an offline-first architecture, when should an item in the outbox ObjectStore be deleted?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP