JSON Nexus Visualizer Phase 1 Complete: Web Worker Offloading, IndexedDB Persistence & Responsive Layout

Production JSON workbenches face two hard constraints: the main thread must stay responsive at 60 fps, and local persistence must survive 10 MB payloads without quota exceptions.

In our foundational guide, we engineered JSON Nexus Visualizer from a Node 22 prototype to a production WordPress plugin. In the first upgrade cycle, we shipped Web Worker parser offloading and a responsive full-viewport flex layout.

This tutorial completes Phase 1 with the final critical upgrade: IndexedDB session storage migration—replacing volatile localStorage with asynchronous, large-capacity storage that handles 10 MB workspaces without quota errors. We’ll walk through the complete storage layer, the async initialization pattern, and the WordPress plugin integration.


Phase 1 Recap: Three Upgrades, One Goal

  • Item 1 — Web Worker Parser Offloading: Moved JSON parsing, flattening, profiling, and duplicate-key detection to a dedicated Web Worker. Main thread stays responsive; 15 K rows parse in background with zero UI jank.
  • Item 2 — IndexedDB Session Storage Migration: Replaced 5 MB localStorage quota with IndexedDB (hundreds of MB). Automatic migration, async load/save, graceful fallback.
  • Item 3 — Responsive Full-Viewport Layout: Flexbox chain from body → .app-shell → #tablePanel → .table-wrap with min-height: 0 enabling true viewport fill on all devices.

All three are live at webdevelopmentor.com/json-nexus/ and in every share link (/s/{token}/).


Why IndexedDB? The localStorage Quota Wall

localStorage is synchronous, capped at ~5 MB per origin, and blocks the main thread on every read/write. A 10 MB JSON payload plus UI state (columns, sorts, profiles, schema, monitor history) exceeds this ceiling regularly, throwing QuotaExceededError and silently losing user work.

IndexedDB solves this:

  • Asynchronous API — no main-thread blocking on read/write.
  • Hundreds of MB quota — browser-managed, scales with available disk.
  • Transactional — atomic puts, no partial writes.
  • Indexable — ready for future multi-workspace support.

Step 1: The IndexedDB Storage Layer (storage-idb.js)

A self-contained module exposing a tiny global API: JNV_Storage.init(), .load(), .save(), .clear(). No external dependencies.

Database Schema

const IDB_DB_NAME = 'json-nexus-visualizer';
const IDB_STORE_NAME = 'state';
const IDB_VERSION = 1;
// Single key: 'workspace-state' holds the complete merged state object

Initialization with Automatic Migration

function initIndexedDB() {
  if (idbInitPromise) return idbInitPromise;

  idbInitPromise = new Promise((resolve, reject) => {
    if (!window.indexedDB) {
      console.warn('[JNV] IndexedDB not supported, falling back to localStorage');
      idbAvailable = false;
      idbReady = true;
      resolve(false);
      return;
    }

    const request = indexedDB.open(IDB_DB_NAME, IDB_VERSION);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
        db.createObjectStore(IDB_STORE_NAME);
      }
    };

    request.onsuccess = (event) => {
      idbAvailable = true;
      idbReady = true;
      console.log('[JNV] IndexedDB initialized successfully');
      resolve(true);
    };

    request.onerror = (event) => {
      console.warn('[JNV] IndexedDB init failed, falling back to localStorage:', event.target.error);
      idbAvailable = false;
      idbReady = true;
      resolve(false);
    };
  });

  return idbInitPromise;
}

Key design points:

  • Promise-based singleton init — safe to call multiple times; returns cached promise.
  • Graceful degradation — if indexedDB absent or blocked, sets idbAvailable = false and resolves false; callers fall back to localStorage.
  • No schema migration complexity — single object store, single key; state object is versioned via DEFAULT_STATE merge in loadState().

Legacy localStorage Migration

Runs once on first load if IndexedDB is available:

async function migrateLocalStorageToIDB() {
  if (!idbAvailable) return false;

  try {
    const legacyData = localStorage.getItem(STORAGE_KEY); // 'json-nexus:state:v1'
    if (!legacyData) return false;

    const db = await getIDB();
    const tx = db.transaction(IDB_STORE_NAME, 'readwrite');
    const store = tx.objectStore(IDB_STORE_NAME);

    await new Promise((resolve, reject) => {
      const putRequest = store.put(JSON.parse(legacyData), 'workspace-state');
      putRequest.onsuccess = () => resolve();
      putRequest.onerror = () => reject(putRequest.error);
    });

    localStorage.setItem(STORAGE_KEY + ':migrated', 'true');
    console.log('[JNV] Migrated localStorage to IndexedDB');
    return true;
  } catch (err) {
    console.warn('[JNV] Migration failed:', err);
    return false;
  }
}

We keep the localStorage copy (with a :migrated flag) for multi-tab sync and as a safety net. The migration is idempotent and non-destructive.

Load / Save / Clear

async function loadStateIDB() {
  await waitForIDB();

  if (idbAvailable) {
    try {
      const db = await getIDB();
      const tx = db.transaction(IDB_STORE_NAME, 'readonly');
      const store = tx.objectStore(IDB_STORE_NAME);
      const request = store.get('workspace-state');

      const saved = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });

      if (saved) {
        return mergeWithDefaults(saved); // merges with DEFAULT_STATE
      }
    } catch (err) {
      console.warn('[JNV] IndexedDB load failed, trying localStorage:', err);
    }
  }

  // Fallback to localStorage (identical merge logic)
  return loadFromLocalStorage();
}

async function persistStateIDB() {
  await waitForIDB();

  const payload = { ...state, rawInput: els.jsonInput?.value ?? state.rawInput };

  // Primary: IndexedDB
  if (idbAvailable) {
    try {
      const db = await getIDB();
      const tx = db.transaction(IDB_STORE_NAME, 'readwrite');
      const store = tx.objectStore(IDB_STORE_NAME);
      await new Promise((resolve, reject) => {
        const putRequest = store.put(payload, 'workspace-state');
        putRequest.onsuccess = () => resolve();
        putRequest.onerror = () => reject(putRequest.error);
      });
    } catch (err) {
      console.warn('[JNV] IndexedDB save failed:', err);
    }
  }

  // Secondary: localStorage (sync for multi-tab, fallback)
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
    els.saveStatus.textContent = `Auto-saved ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
  } catch (err) {
    console.warn('[JNV] localStorage save failed:', err);
    els.saveStatus.textContent = 'Local storage unavailable';
  }
}

async function clearStateIDB() {
  await waitForIDB();
  if (idbAvailable) { /* delete from IDB */ }
  localStorage.removeItem(STORAGE_KEY);
  localStorage.removeItem(STORAGE_KEY + ':migrated');
}

Step 2: Async State Initialization in app.js

The app can no longer synchronously call loadState() at module top level. Instead, we defer state hydration to an async IIFE that runs after DOM ready.

State Declaration (No Sync Load)

// State will be loaded asynchronously in the init function
let state = { ...DEFAULT_STATE };
let storageReady = false;

Async Load / Persist Wrappers

async function loadState() {
  if (window.JNV_Storage && window.JNV_Storage.load) {
    storageReady = true;
    return await window.JNV_Storage.load();
  }
  // Fallback to localStorage (same merge logic)
  try {
    const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
    return mergeWithDefaults(saved || {});
  } catch {
    return { ...DEFAULT_STATE };
  }
}

async function persistState() {
  if (storageReady && window.JNV_Storage && window.JNV_Storage.save) {
    await window.JNV_Storage.save();
    return;
  }
  // Fallback to localStorage
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...state, rawInput: els.jsonInput?.value ?? state.rawInput }));
    els.saveStatus.textContent = `Auto-saved ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
  } catch {
    els.saveStatus.textContent = 'Local storage unavailable';
  }
}

async function resetAll() {
  stopMonitoring();
  if (storageReady && window.JNV_Storage && window.JNV_Storage.clear) {
    await window.JNV_Storage.clear();
  } else {
    localStorage.removeItem(STORAGE_KEY);
    localStorage.removeItem(STORAGE_KEY + ':migrated');
  }
  Object.assign(state, clone(DEFAULT_STATE));
  // ... reset UI, render, toast ...
}

Startup IIFE: Load → Hydrate UI → Init Worker → Render

(async () => {
  // 1. Load persisted state FIRST
  state = await loadState();

  // 2. Hydrate UI inputs from loaded state
  Object.assign(els.jsonInput, { value: state.rawInput || '' });
  Object.assign(els.endpointInput, { value: state.endpointUrl || '' });
  Object.assign(els.filterInput, { value: state.filter || '' });
  Object.assign(els.pageSize, { value: String(state.pageSize || 25) });
  Object.assign(els.pageJump, { value: String(state.page || 1) });

  // 3. Initialize Web Worker
  initParserWorker();

  // 4. Handle share token or render
  const activeToken = getActiveShareToken();
  if (activeToken) {
    isSharedView = true;
    sharedAllowModification = false;
    applySharePresentation();
    await loadShareFromPath(activeToken);
  } else if (state.rawInput) {
    await tryParseInput(state.rawInput, { show: true, autoProfile: true });
  } else {
    render();
  }
})();

Order matters: UI must reflect loaded state before the worker starts or the first parse runs, otherwise users see a flash of empty state.


Step 3: WordPress Plugin Integration

Enqueue Storage Script as Dependency

// json-nexus-visualizer.php — jnv_enqueue_assets()

wp_enqueue_script(
    'json-nexus-storage',
    JSON_NEXUS_URL . 'assets/bbs/js/storage-idb.js',
    [],
    JSON_NEXUS_VERSION,
    ['strategy' => 'defer']
);

wp_enqueue_script(
    'json-nexus-app',
    JSON_NEXUS_URL . 'assets/app.js',
    ['json-nexus-profiler', 'json-nexus-schema-validator', 'json-nexus-monitor', 'json-nexus-table-export', 'json-nexus-storage'],
    JSON_NEXUS_VERSION,
    ['strategy' => 'defer']
);

The storage script loads before app.js (via dependency array), so window.JNV_Storage exists when the startup IIFE runs.

Standalone Share Template

// visualizer-standalone.php — inline script tags





Share links (/s/{token}/) get the same storage layer, so users who bookmark a shared visualization can reload it and their UI preferences (theme, column order, density) persist locally.


Verification Checklist

  1. IndexedDB opens: DevTools → Application → IndexedDB → json-nexus-visualizer → state → key workspace-state present.
  2. Migration runs once: Console shows [JNV] Migrated localStorage to IndexedDB on first visit after upgrade; :migrated flag set in localStorage.
  3. 10 MB payload persists: Drop a 4–8 MB JSON file; refresh page; workspace restores with theme, column order, filter, sort intact.
  4. Multi-tab sync: Open two tabs; change theme in one; refresh other → theme updates (via localStorage sync).
  5. Fallback works: Disable IndexedDB via Chrome DevTools → Application → Clear storage → uncheck IndexedDB; reload; workspace still loads from localStorage.
  6. Share link persistence: Open /s/{token}/ in incognito; change density; close/reopen → density preserved.
  7. Reset clears both: Click Reset button; IndexedDB key deleted; localStorage key + :migrated flag removed.

Complete Phase 1 Architecture

LayerFilePurpose
Storageassets/bbs/js/storage-idb.jsIndexedDB + localStorage fallback, migration, async API
Workerassets/bbs/js/parser-worker.jsOff-thread parse, flatten, profile, duplicate-key stats
App Coreassets/app.jsAsync state init, worker delegation, UI hydration, render loop
Profilingassets/bbs/js/profiler.jsArchetype detection, schema hints, layout strategy
Validationassets/bbs/js/schema-validator.jsDraft-07 JSON Schema validator
Monitoringassets/bbs/js/monitor.jsREST polling, diffing, change highlighting
Exportassets/bbs/js/table-export.jsCSV, Markdown, HTML, SVG, PNG, PDF
Stylesassets/styles.cssFlexbox layout, themes, responsive breakpoints
WP Pluginjson-nexus-visualizer.phpEnqueue, localize, REST endpoints, shortcode, block
Templatestemplates/visualizer-embed.php, visualizer-standalone.phpShortcode/block embed + standalone share view

Try the Complete Phase 1 Tool

All three upgrades are live. Drop a 10 MB JSON file, watch the worker parse it without freezing the UI, resize to mobile viewport, refresh — your workspace is exactly as you left it.

The complete plugin (GPLv3) includes the PHP PDO SQLite backend, 90-day cryptographic share links, REST monitoring, JSON Schema validation, and all Phase 1 client-side upgrades. Download the plugin ZIP from the tool page or clone the repository for self-hosted deployment.

// THE TRANSMISSION LOG : FIELD NOTES

Weekly zero-fluff breakdowns of distributed systems, browser runtime performance, and production post-mortems delivered directly from Stella Sage.