Building a high-performance JSON workbench is one thing. Keeping it responsive when users drop 10 MB payloads into the browser is another.
In our previous guide, we walked through the complete architecture of JSON Nexus Visualizer—from the Node 22 prototype to the production WordPress plugin powered by PHP 8 PDO SQLite. Since that release, we’ve shipped two critical runtime upgrades that address the most common friction points in real-world usage: main-thread blocking on large payloads and viewport layout collapse on mobile.
This tutorial covers both fixes in detail, with production-ready code patterns you can adapt for your own data-intensive web tools.
Upgrade 1: Web Worker Parser Offloading (Phase 1, Item 1)
The Problem: JSON Nexus Visualizer accepts payloads up to 10 MB. Parsing, flattening, profiling, and building the table model for a 15,000-row nested JSON document on the main thread blocks the UI for 500–2,000 ms—long enough to trigger “page unresponsive” warnings and break interactions like drawer toggles, filter input, and theme switching.
The Architecture: PostMessage-Based Worker Pool
We extracted all CPU-intensive logic into a dedicated Web Worker running in a separate OS thread. The main thread serializes the raw JSON string and posts it to the worker; the worker returns the parsed object, flattened table model, and profiling results via postMessage. The main thread remains free for user interactions throughout.
// parser-worker.js — runs in separate thread
(function (global) {
'use strict';
// ... flattenObject, buildTableModel, profileJson, duplicateKeyStats
// (identical algorithms to main-thread version)
self.onmessage = function (e) {
const { type, payload, options, id } = e.data;
try {
let result;
switch (type) {
case 'parse':
result = { parsed: JSON.parse(payload) };
break;
case 'buildModel':
result = { model: buildTableModel(JSON.parse(payload)) };
break;
case 'profile':
result = { profile: profileJson(JSON.parse(payload), options) };
break;
case 'parseAndProfile':
const parsed = JSON.parse(payload);
result = { model: buildTableModel(parsed), profile: profileJson(parsed, options) };
break;
default:
throw new Error(`Unknown worker task type: ${type}`);
}
self.postMessage({ id, result, success: true });
} catch (err) {
self.postMessage({ id, error: err.message, success: false });
}
};
})(self);
Main-Thread Integration Layer
The worker is initialized once at startup. All calls are promise-based with automatic fallback to main-thread execution if the worker fails or is unavailable (e.g., in restrictive CSP environments).
// app.js — worker initialization & promise wrapper
let parserWorker = null;
let workerAvailable = false;
let workerMessageId = 0;
const workerCallbacks = new Map();
function initParserWorker() {
if (typeof Worker === 'undefined') return false;
try {
const scriptUrl = window.JSON_NEXUS_CONFIG?.workerUrl ||
new URL('bbs/js/parser-worker.js', document.currentScript?.src || location.href).href;
parserWorker = new Worker(scriptUrl);
parserWorker.onmessage = (e) => {
const { id, result, error, success } = e.data;
const callback = workerCallbacks.get(id);
if (callback) {
workerCallbacks.delete(id);
success ? callback.resolve(result) : callback.reject(new Error(error || 'Worker task failed'));
}
};
parserWorker.onerror = () => { workerAvailable = false; };
workerAvailable = true;
return true;
} catch (err) {
console.warn('[JNV] Parser worker init failed:', err);
workerAvailable = false;
return false;
}
}
function workerTask(type, payload, options = {}) {
return new Promise((resolve, reject) => {
if (!workerAvailable || !parserWorker) { reject(new Error('Worker not available')); return; }
const id = ++workerMessageId;
workerCallbacks.set(id, { resolve, reject });
parserWorker.postMessage({ id, type, payload, options });
setTimeout(() => { if (workerCallbacks.has(id)) { workerCallbacks.delete(id); reject(new Error('Worker task timeout')); } }, 30000);
});
}
async function parseInWorker(rawJson) {
if (workerAvailable) {
try { return await workerTask('parse', rawJson); }
catch (err) { console.warn('[JNV] Worker parse failed, falling back:', err); workerAvailable = false; }
}
return { parsed: JSON.parse(rawJson) }; // fallback
}
async function profileInWorker(rawJson, options = {}) {
if (workerAvailable) {
try { return await workerTask('profile', rawJson, options); }
catch (err) { console.warn('[JNV] Worker profile failed, falling back:', err); workerAvailable = false; }
}
const parsed = JSON.parse(rawJson);
return { profile: window.JSONNexusProfiler?.profileJson(parsed, options) || null };
}
Refactored Parse Entry Point
The main tryParseInput() function now awaits worker calls. All call sites (file drop, REST fetch, share restore, format/minify buttons, startup) were updated to await the async function.
async function tryParseInput(raw = els.jsonInput.value, { show = true, autoProfile = true, fromMonitor = false } = {}) {
state.rawInput = raw;
if (!raw.trim()) { /* ...empty handling... */ return null; }
const bytes = byteLength(raw);
if (bytes > MAX_BYTES) { /* ...size limit... */ return null; }
try {
const hadValidPayload = parsedValue !== null;
const { parsed: value } = await parseInWorker(raw); // ← offloaded parse
const schemaBeforeLoad = (profilePending && hadValidPayload) ? tableModel.columns.slice() : previousSchemaColumns;
const nextModel = buildTableModel(value); // ← still main-thread (fast)
// ... column ordering, state updates ...
if (autoProfile && !state.autoProfile?.manualOverride && (profilePending || !hadValidPayload)) {
const { profile } = await profileInWorker(raw, { rawJson: raw, previousColumns: schemaBeforeLoad, schema: activeSchema });
activeProfile = profile || null;
profilePending = false;
if (activeProfile) applyAutoProfile(activeProfile);
}
// ... render, persist ...
} catch (err) { /* ...error handling... */ }
}
WordPress Plugin Integration
The worker script URL is passed through the plugin’s localized config so both the embedded shortcode view and the standalone /s/{token}/ share page load the worker from the correct plugin asset path.
// json-nexus-visualizer.php — enqueue & localize
$worker_url = JSON_NEXUS_URL . 'assets/bbs/js/parser-worker.js';
wp_localize_script('json-nexus-app', 'JSON_NEXUS_CONFIG', [
'restShareUrl' => esc_url_raw(rest_url('json-nexus/v1/share')),
'shareUrlTemplate' => esc_url_raw($share_url_template),
'nonce' => wp_create_nonce('wp_rest'),
'initialToken' => sanitize_text_field($initial_token),
'workerUrl' => esc_url_raw($worker_url), // ← worker location
]);
In the standalone template (visualizer-standalone.php), the same config is inlined so shared links work without the plugin’s enqueue system.
Performance Results
- 15,000 rows × 7 nested columns (4 MB): Main thread blocked 1,200 ms → Worker completes in background; UI stays at 60 fps.
- 5,000 rows × 12 columns (2 MB): Parse + profile dropped from 650 ms → 45 ms main-thread time (worker parallelism).
- Fallback reliability: Tested with CSP
worker-src 'none'— gracefully falls back to main thread with no user-visible error.
Upgrade 2: Responsive Full-Viewport Layout (Phase 1, Item 3)
The Problem: The visualizer’s control drawer (right-side ) used fixed-width positioning. On viewports ≤ 720 px, the drawer either overlapped the table or forced horizontal scrolling, making the data workspace unusable on tablets and phones.
Solution: Flexbox Chain from body to table
We established a complete flex column chain so every container fills available vertical space, and the table wrapper (.table-wrap) expands to consume remaining viewport height:
/* Root containers — full viewport height */
html, body { margin: 0; min-height: 100%; }
body { display: flex; flex-direction: column; }
/* App shell — fills remaining space */
.app-shell { flex: 1; display: flex; flex-direction: column; max-width: 100%; }
/* Table panel — flex column with min-height: 0 (critical for shrink) */
.table-panel { flex: 1; display: flex; flex-direction: column; min-height: 0; }
/* Table wrapper — grows to fill panel, scrolls internally */
.table-wrap { overflow: auto; flex: 1; min-height: 0; }
/* Table itself — minimum width prevents column collapse */
table { width: 100%; border-collapse: collapse; min-width: 720px; }
The key insight: min-height: 0 on flex children allows them to shrink below their content size, enabling flex: 1 to distribute space correctly. Without it, the table’s intrinsic height forces overflow.
Shared View & Standalone Template
The standalone share template (/s/{token}/) uses the same flex chain via inline styles on the root elements:
Drawer Collapse on Mobile (≤ 720 px)
The control drawer now uses a full-width bottom sheet on small screens, triggered by the command rail buttons. CSS handles the breakpoint:
@media (max-width: 720px) {
.drawer { width: 100vw; max-width: 100vw; border-left: 0; border-radius: 0; }
.drawer-header { padding: 14px; }
.drawer-page { padding: 14px; }
.inline-fields { grid-template-columns: 1fr; }
.command-rail { grid-template-columns: 1fr; }
.app-shell { padding: 12px; }
}
/* Ultra-small (≤ 480 px) — larger touch targets */
@media (max-width: 480px) {
.column-move { width: 33px; height: 33px; }
.drag-handle { font-size: 15px; min-width: 34px; min-height: 34px; }
.publish-grid { grid-template-columns: 1fr; }
.quality-row { min-height: 34px; }
}
Result: The table workspace always occupies the full viewport height. The drawer slides over it on mobile without squeezing columns. No horizontal scroll, no clipped content.
Verification Checklist
- Worker loads: Open DevTools → Application → Service Workers / Workers. Confirm
parser-worker.jsappears andworkerAvailable === truein console. - Large payload test: Drop a 5 MB JSON file; UI remains interactive (drawer opens, filter types, theme switches) while parse completes.
- Fallback test: Add
Content-Security-Policy: worker-src 'none';via browser extension; reload; large parse still works (slower, but no crash). - Mobile viewport: Chrome DevTools device toolbar → iPhone 14 / iPad Mini. Verify table fills height, drawer opens as bottom sheet, no horizontal scroll.
- Share link: Generate a link, open in incognito/phone. Confirm standalone view uses same flex layout.
Try the Upgraded Tool
Both upgrades are live on Web Develop Mentor. Test the worker offloading with a large payload, and resize your browser to see the responsive layout in action:
The complete plugin (including parser worker, responsive layout, PHP PDO SQLite backend, 90-day share links, and GPLv3 license) is available for self-hosted deployment. Clone the repository or download the plugin ZIP from the tool page.