Developers often rely on JSON viewers to parse API responses, but most tools stop at pretty-printing. These viewers add indentation and syntax highlighting, which works for small configuration files but becomes unusable for large datasets. A typical API response with 50 records can balloon to 500 lines of text, requiring tedious scrolling to locate specific data points.
The fundamental issue isn't the viewer—it's the format itself. Arrays of objects are better represented as tables, where columns can be sorted, rows can be searched, and nested details can be expanded on demand. This approach transforms raw data into something scannable rather than readable, drastically improving workflow efficiency.
To address this gap, I built a tool called prettyjsonxml.com, a JSON and XML viewer that converts arrays into interactive tables. The tool also supports a foldable tree view, format and minify options, and base64 image previews. It runs entirely in the browser with no backend, ensuring user data remains local. Surprisingly, the biggest challenge wasn't building the table view—it was optimizing performance for large files, particularly a 9 MB API response.
Why traditional JSON viewers fall short
Most JSON viewers share a common workflow: they parse the input, apply syntax highlighting, and display the result. While this works for small files, it fails when dealing with real-world API responses. A 50-row array with nested objects can sprawl across hundreds of lines, making it nearly impossible to locate a specific record or filter data dynamically. Users are forced to scroll top-to-bottom, re-reading each line to find relevant information.
The core problem isn't visual formatting—it's that arrays of objects demand a tabular representation. Spreadsheets handle this naturally; JSON viewers should too. By converting arrays into tables with sortable columns and searchable rows, users can instantly locate admins, sort by date, or filter active users without parsing endless text.
The first attempt: simplicity meets performance limits
The initial version of the tool was straightforward. It parsed the JSON input, extracted the array of objects, and rendered each record as a table row. For small datasets, this worked flawlessly. However, when tested with a real API response—specifically a 9 MB file containing 30,000 records—the browser froze for 1.5 seconds.
The freeze stemmed from two bottlenecks:
- JSON parsing: The
JSON.parseoperation blocked the main thread for approximately 500 milliseconds. - DOM rendering: Creating 60,000 nodes (30,000 main rows + 30,000 detail rows) consumed an additional 1,500 milliseconds.
These delays were unacceptable, as they disrupted the user experience entirely. The goal shifted from "does it work?" to "how can we make it feel instantaneous?"
Optimizing perceived performance with CSS containment
The first breakthrough came from leveraging modern CSS features. The browser's content-visibility: auto property allows developers to instruct the rendering engine to defer layout calculations for off-screen content. By combining this with contain-intrinsic-size, the browser can maintain scrollbar accuracy while prioritizing visible content.
.data-table tr.row-main {
content-visibility: auto;
contain-intrinsic-size: auto 40px;
}This single line of CSS didn't reduce the total rendering time—it simply reordered the work. The browser now paints visible rows first, giving users the impression of near-instant responsiveness. While this technique works in approximately 95% of modern browsers, it’s surprisingly underused despite its simplicity and effectiveness.
The Web Worker paradox: when cloning slows things down
The next logical step was offloading the JSON.parse operation to a Web Worker to keep the main thread responsive. The approach seemed sound:
const worker = new Worker(
URL.createObjectURL(
new Blob([`
self.onmessage = (e) => {
const parsed = JSON.parse(e.data);
self.postMessage(parsed);
};
`], { type: 'application/javascript' })
)
);
worker.postMessage(largeJsonString);
worker.onmessage = (e) => render(e.data);However, the results were counterintuitive. Moving the parsing off the main thread did reduce blocking time, but the structured-clone operation required to transfer the parsed object back to the main thread added 400 milliseconds of its own. For a 30,000-object array, this meant replacing a 500-millisecond main-thread delay with a 400-millisecond delay—barely a net gain.
The lesson was clear: Web Workers excel when the result is lightweight, such as a formatted string for minification. When the payload is a large object graph, structured cloning negates the benefits. The real solution lay elsewhere.
Virtual scrolling: the game-changer for large datasets
The breakthrough came from implementing virtual scrolling, a technique that renders only the rows visible in the viewport and swaps them dynamically as the user scrolls. The challenge was adapting this approach to HTML tables, which don’t support absolute positioning of rows.
The solution involved using spacer rows to maintain scrollbar accuracy while dynamically inserting and removing visible rows:
<table>
<thead>
<!-- Column headers -->
</thead>
<tbody>
<tr style="height:850px"></tr>
<!-- Spacer for rows above viewport -->
<tr>row 21</tr>
<tr>row 22</tr>
<!-- Visible rows -->
<tr>row 70</tr>
<tr style="height:1200px"></tr>
<!-- Spacer for rows below viewport -->
</tbody>
</table>The scroll event handler calculates which rows are visible and updates the DOM accordingly:
function onScroll() {
const tbodyRect = tbody.getBoundingClientRect();
const viewTop = Math.max(0, -tbodyRect.top);
const viewBottom = viewTop + scrollContainer.clientHeight;
const startRow = Math.max(0, Math.floor(viewTop / ROW_HEIGHT) - 10);
const endRow = Math.min(items.length, Math.ceil(viewBottom / ROW_HEIGHT) + 10);
// Remove old rows and render new ones from items[startRow..endRow]
// Adjust spacer heights to maintain scrollbar position
}The result was transformative. A 30,000-row dataset that previously froze the browser now scrolls smoothly at 60 frames per second. Even smaller datasets benefit from instant search and sort operations, as these now operate on the JavaScript array rather than traversing thousands of DOM nodes.
Lessons learned: subtleties that matter
One subtle but critical lesson emerged during testing: scroll events don’t always come from the expected source. In one instance, the tool’s scroll container had overflow: hidden on the body and overflow: auto on the container itself. This setup caused the window.scroll event to never fire, as scrolling was handled by the container. The fix required identifying the nearest scrollable ancestor dynamically:
function findScrollContainer(el) {
let p = el.parentElement;
while (p && p !== document.body) {
const oy = getComputedStyle(p).overflowY;
if ((oy === 'auto' || oy === 'scroll') && p.scrollHeight > p.clientHeight) {
return p;
}
p = p.parentElement;
}
return document.body;
}This underscores a broader principle: performance optimizations often reveal edge cases that break assumptions. Virtual scrolling, CSS containment, and Web Workers are powerful tools, but their effectiveness depends on addressing subtle implementation details.
Looking ahead: what’s next for JSON/XML viewers?
The tool’s current iteration proves that arrays of objects belong in tables, not text blocks. Future enhancements could include real-time collaboration features, deeper integration with developer tools, or AI-assisted query suggestions. The key takeaway, however, is that the right format can transform how developers interact with data—turning endless scrolling into instant insights.
AI summary
Traditional JSON viewers fail with large datasets. Discover a tool that converts API responses into interactive tables for instant scanning, sorting, and searching.