How to Render and Query a 25 MB / 60,000-Node JSON Tree in the Browser at 60 FPS

If you have ever pasted a 10 MB JSON response into a browser-based JSON viewer and watched the tab freeze with an "Aw, Snap! Out of Memory" crash, the culprit is almost never JSON.parse() itself.

In V8, parsing 10 MB of JSON takes ~60 milliseconds. What kills the browser tab is DOM node explosion and main-thread blocking:

  1. A 10 MB JSON payload easily contains 50,000 to 150,000 keys and array elements. Creating 3 <div> and <span> elements per node injects 300,000+ DOM nodes into the document tree, exhausting layout and style recalculation budgets.
  2. Running syntax repair, key sorting, and search indexing on the UI thread blocks requestAnimationFrame for seconds at a time.

Here is how we architected a client-side JSON workbench that formats, repairs, queries, and renders up to 25 MB payloads at 60 FPS using Web Workers, flat tree virtualization, and a 4-stage dirty-JSON repair pipeline.

Step 1: Offloading Payloads > 256 KB to an Inline Web Worker

For small payloads under 256 KB, synchronous parsing on the main thread takes less than 4 ms. Once a payload crosses 256 KB (up to our 25 MB browser safety ceiling), every heavy operation—strict parsing, lexical diagnostics, recursive key sorting, and stats calculation—should run off the main thread:

const MAX_INPUT_BYTES = 25 * 1024 * 1024; // 25 MB hard cap const WORKER_THRESHOLD = 256 * 1024; // 256 KB triggers Web Worker 

By serializing the worker script into a Blob URL (URL.createObjectURL(new Blob([workerSource]))), the worker runs 100% locally in the browser without requiring external network fetches, keeping the main UI thread responsive while typing or scrolling.

Step 2: Flattening the Hierarchy for 28px Virtualized Row Recycling

Instead of recursively rendering nested <ul><li> components in React, we flatten only the currently expanded tree branches into a contiguous 1D array of VisibleTreeRow descriptors:

export interface VisibleTreeRow { path: string; // e.g., "$.users[0].profile" key: string | null; value: JsonValue; depth: number; isExpandable: boolean; childCount: number; summary: string; // e.g., "{ 4 keys }" or "[ 120 items ]" } 

With a fixed row height (ROW_HEIGHT = 28 px) and an overscan buffer (OVERSCAN = 12 rows above and below the scroll viewport), a tree with 60,000 expanded rows only mounts ~45 actual DOM nodes at any given millisecond:

const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); const endIndex = Math.min( visibleRows.length, Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) + OVERSCAN ); const windowedRows = visibleRows.slice(startIndex, endIndex); 

Step 3: Repairing "Dirty" JavaScript & Python Logs into Valid RFC 8259 JSON

Developers frequently copy broken JSON from terminal logs, Python dicts, or JavaScript source files containing:

  • Single-line (//) and block (/* ... */) comments
  • Single-quoted strings ('status': 'active')
  • Unquoted object keys ({ userId: 42, active: true })
  • Python/JS non-JSON literals (None, True, False, undefined, NaN, Infinity)
  • Trailing commas before } or ]

A naive .replace(/'/g, '"') corrupts strings that contain apostrophes ("Don't"). Instead, a deterministic 4-pass character lexer (repairJson) tracks string context so it only converts outer single quotes while escaping internal double quotes (\"), strips comments outside string literals, quotes bare identifier keys, and strips trailing commas.

You can inspect this virtualized tree architecture, test the 4-pass auto-repair engine, or run live JSONPath filter queries ($.items[?(@.price < 50)]) directly in your browser with the KivaroTools JSON Formatter, Validator & Tree Viewer.