From df679bb89282e6255b86ff05000037bb3c077fbd Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Fri, 5 Jun 2026 13:14:10 -0700 Subject: [PATCH 1/4] Add support for IndexedDB --- .../persist-structured-data/demo.html | 242 ++++++++++++++++++ .../persist-structured-data/expectations.md | 10 + .../persist-structured-data/guide.md | 161 ++++++++++++ 3 files changed, 413 insertions(+) create mode 100644 guides/performance/persist-structured-data/demo.html create mode 100644 guides/performance/persist-structured-data/expectations.md create mode 100644 guides/performance/persist-structured-data/guide.md diff --git a/guides/performance/persist-structured-data/demo.html b/guides/performance/persist-structured-data/demo.html new file mode 100644 index 000000000..b15eb5eac --- /dev/null +++ b/guides/performance/persist-structured-data/demo.html @@ -0,0 +1,242 @@ + + + + + + IndexedDB Notes Demo + + + +

Notes

+

Add notes below. They persist in IndexedDB across page reloads.

+ +
+ + +
+ + + + + + diff --git a/guides/performance/persist-structured-data/expectations.md b/guides/performance/persist-structured-data/expectations.md new file mode 100644 index 000000000..2da4317e0 --- /dev/null +++ b/guides/performance/persist-structured-data/expectations.md @@ -0,0 +1,10 @@ +- `indexedDB.open()` is called with a database name and a numeric version. +- Object stores are created inside the `upgradeneeded` event handler using `createObjectStore()`. +- At least one index is created with `createIndex()` for efficient querying. +- Data reads use `"readonly"` transactions; data writes use `"readwrite"` transactions. +- IndexedDB operations are wrapped in Promises for ergonomic async/await usage. +- The `versionchange` event is handled on the database connection to close it when another tab upgrades. +- `put()` is used for insert-or-update operations; `add()` is used when duplicate keys should error. +- Error handling is present on transactions or requests (e.g., `onerror` handlers or Promise rejections). +- Sensitive data (tokens, passwords) is NOT stored in IndexedDB without encryption. +- Simple string key-value storage does NOT use IndexedDB when `localStorage` would suffice. diff --git a/guides/performance/persist-structured-data/guide.md b/guides/performance/persist-structured-data/guide.md new file mode 100644 index 000000000..fe3428a5c --- /dev/null +++ b/guides/performance/persist-structured-data/guide.md @@ -0,0 +1,161 @@ +--- +name: persist-structured-data +description: Store and retrieve structured application data client-side using IndexedDB, enabling offline access, fast local reads, and reduced network dependency without relying on cookies or localStorage for complex data. +web-feature-ids: + - indexeddb +sources: + - https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API + - https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB +--- + +# Persist structured data with IndexedDB + +When your application needs to store structured data client-side — for offline access, caching API responses, or persisting user-generated content across sessions — IndexedDB is the appropriate storage mechanism. Unlike `localStorage`, which is limited to string key-value pairs and blocks the main thread, IndexedDB supports structured objects, indexes for efficient querying, and large storage quotas via an asynchronous, transactional API. + +## How to implement + +1. **Open (or create) a database:** Call `indexedDB.open()` with a database name and version number. If the database doesn't exist or the version is higher than the current one, the `upgradeneeded` event fires, giving you the opportunity to define or update the schema. + +2. **Define object stores and indexes in `upgradeneeded`:** Create object stores (analogous to tables) with a `keyPath` or `autoIncrement` key generator. Add indexes on properties you need to query by. + +3. **Read and write data through transactions:** All data access goes through transactions. Use `"readwrite"` transactions for writes (`put`, `add`, `delete`) and `"readonly"` transactions for reads (`get`, `getAll`, `openCursor`). + +4. **Handle version changes gracefully:** Listen for the `versionchange` event on the database connection so you can close it when another tab upgrades the schema. + +## Example code + +This example stores and retrieves a collection of notes. The database uses `autoIncrement` for keys and an index on the `updatedAt` property to support ordering by recency. + +```javascript +const DB_NAME = "app-notes"; +const DB_VERSION = 1; +const STORE_NAME = "notes"; + +function openDatabase() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + // Only create the store if it doesn't already exist. + if (!db.objectStoreNames.contains(STORE_NAME)) { + const store = db.createObjectStore(STORE_NAME, { + keyPath: "id", + autoIncrement: true, + }); + // Index for querying notes by last-updated time. + store.createIndex("updatedAt", "updatedAt", { unique: false }); + } + }; + + request.onsuccess = (event) => { + const db = event.target.result; + + // MANDATORY: Handle version changes from other tabs. + db.onversionchange = () => { + db.close(); + }; + + resolve(db); + }; + + request.onerror = (event) => { + reject(request.error); + }; + }); +} + +async function addNote(db, note) { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readwrite"); + const store = tx.objectStore(STORE_NAME); + + const record = { ...note, updatedAt: Date.now() }; + const request = store.add(record); + + request.onsuccess = () => resolve(request.result); + tx.onerror = () => reject(tx.error); + }); +} + +async function getAllNotes(db) { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readonly"); + const store = tx.objectStore(STORE_NAME); + const index = store.index("updatedAt"); + + // Retrieve all notes ordered by updatedAt (ascending). + const request = index.getAll(); + + request.onsuccess = () => resolve(request.result); + tx.onerror = () => reject(tx.error); + }); +} + +async function deleteNote(db, id) { + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readwrite"); + const store = tx.objectStore(STORE_NAME); + + const request = store.delete(id); + + request.onsuccess = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +// Usage +const db = await openDatabase(); +await addNote(db, { title: "First note", body: "Hello, IndexedDB!" }); +const notes = await getAllNotes(db); +``` + +## Best Practices + +- **DO** wrap IndexedDB operations in Promises or use a thin promise wrapper. The raw event-based API is error-prone and difficult to compose with `async`/`await`. +- **DO** perform all schema changes (creating/deleting object stores and indexes) inside the `upgradeneeded` handler. This is the only context where structural changes are allowed. +- **DO** handle the `versionchange` event on the database connection to close it when another tab upgrades the schema. Failing to do so blocks the upgrade. +- **DO** keep transactions short-lived. A transaction becomes inactive as soon as control returns to the event loop without a pending request on it. +- **DO** use `put()` when you want insert-or-update semantics. Use `add()` only when you want an error if the key already exists. +- **DO** use indexes and key ranges (`IDBKeyRange`) for efficient queries instead of iterating all records with a cursor. +- **DO** use `"readonly"` transactions for reads. Multiple readonly transactions can run concurrently, but only one `"readwrite"` transaction per object store is active at a time. +- **DO NOT** store sensitive data (tokens, passwords, PII) in IndexedDB without encryption. IndexedDB is not a secure store — any script running on the origin can access it. +- **DO NOT** rely on IndexedDB transactions completing during page `unload` or `beforeunload`. The browser may abort them. +- **DO NOT** use IndexedDB for simple key-value pairs where `localStorage` would suffice. IndexedDB adds complexity that is only justified when you need structured queries, large storage, or non-blocking access. + +## Browser support and fallback strategies + +{{ BASELINE_STATUS("indexeddb") }} + +IndexedDB is Baseline Widely Available and supported in all modern browsers. A fallback strategy is not typically required. + +### Feature detection + +```javascript +if (indexedDB) { + // IndexedDB is available. +} else { + // Extremely old browser — fall back to localStorage for basic persistence. +} +``` + +### Storage quota + +Browsers impose per-origin storage limits. Use the Storage API to check available space before writing large amounts of data: + +```javascript +if (navigator.storage && navigator.storage.estimate) { + const { usage, quota } = await navigator.storage.estimate(); + console.log(`Using ${usage} of ${quota} bytes.`); +} +``` + +To request persistent storage that won't be evicted under storage pressure: + +```javascript +if (navigator.storage && navigator.storage.persist) { + const granted = await navigator.storage.persist(); + console.log(granted ? "Storage is persistent." : "Storage may be evicted."); +} +``` From 3f15a14283663336d831d27c85a227bd2b9bb85a Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Fri, 5 Jun 2026 13:24:09 -0700 Subject: [PATCH 2/4] Fix directory structure --- .../persist-structured-data/demo.html | 0 .../persist-structured-data/expectations.md | 0 .../persist-structured-data/guide.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename guides/{performance => user-experience}/persist-structured-data/demo.html (100%) rename guides/{performance => user-experience}/persist-structured-data/expectations.md (100%) rename guides/{performance => user-experience}/persist-structured-data/guide.md (100%) diff --git a/guides/performance/persist-structured-data/demo.html b/guides/user-experience/persist-structured-data/demo.html similarity index 100% rename from guides/performance/persist-structured-data/demo.html rename to guides/user-experience/persist-structured-data/demo.html diff --git a/guides/performance/persist-structured-data/expectations.md b/guides/user-experience/persist-structured-data/expectations.md similarity index 100% rename from guides/performance/persist-structured-data/expectations.md rename to guides/user-experience/persist-structured-data/expectations.md diff --git a/guides/performance/persist-structured-data/guide.md b/guides/user-experience/persist-structured-data/guide.md similarity index 100% rename from guides/performance/persist-structured-data/guide.md rename to guides/user-experience/persist-structured-data/guide.md From 32f06fac534003b8d25a952c5b616058fb10d780 Mon Sep 17 00:00:00 2001 From: Andrew K Nolan Date: Tue, 30 Jun 2026 14:48:11 -0700 Subject: [PATCH 3/4] Update guides/user-experience/persist-structured-data/guide.md Remove redundant widely available message and reframe fallback verbiage Co-authored-by: Rick Viscomi --- guides/user-experience/persist-structured-data/guide.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/guides/user-experience/persist-structured-data/guide.md b/guides/user-experience/persist-structured-data/guide.md index fe3428a5c..a6a48d0ce 100644 --- a/guides/user-experience/persist-structured-data/guide.md +++ b/guides/user-experience/persist-structured-data/guide.md @@ -128,9 +128,7 @@ const notes = await getAllNotes(db); {{ BASELINE_STATUS("indexeddb") }} -IndexedDB is Baseline Widely Available and supported in all modern browsers. A fallback strategy is not typically required. - -### Feature detection +IndexedDB is supported in all modern browsers. However, if it doesn't meet your Baseline target, use feature detection to check its availability and conditionally fall back to `localStorage` for older browsers. ```javascript if (indexedDB) { From 8f61b72e1d02ecc5ca5b776ee20ec87659060c4c Mon Sep 17 00:00:00 2001 From: Andrew K Nolan Date: Tue, 30 Jun 2026 14:49:01 -0700 Subject: [PATCH 4/4] Update guides/user-experience/persist-structured-data/guide.md Take suggestion that metadata is no longer needed Co-authored-by: Rick Viscomi --- guides/user-experience/persist-structured-data/guide.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/guides/user-experience/persist-structured-data/guide.md b/guides/user-experience/persist-structured-data/guide.md index a6a48d0ce..0915f75c9 100644 --- a/guides/user-experience/persist-structured-data/guide.md +++ b/guides/user-experience/persist-structured-data/guide.md @@ -3,9 +3,6 @@ name: persist-structured-data description: Store and retrieve structured application data client-side using IndexedDB, enabling offline access, fast local reads, and reduced network dependency without relying on cookies or localStorage for complex data. web-feature-ids: - indexeddb -sources: - - https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API - - https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB --- # Persist structured data with IndexedDB