<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aleksandre Mtchedlishvili</title>
    <description>The latest articles on DEV Community by Aleksandre Mtchedlishvili (@aleksandre_mtchedlishvili).</description>
    <link>https://dev.to/aleksandre_mtchedlishvili</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3948960%2F7a6ed252-9a93-47ea-b8e7-8a7df70a0b56.png</url>
      <title>DEV Community: Aleksandre Mtchedlishvili</title>
      <link>https://dev.to/aleksandre_mtchedlishvili</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksandre_mtchedlishvili"/>
    <language>en</language>
    <item>
      <title>Pretty-print isn't enough. I built a JSON &amp; XML viewer with a real table view.</title>
      <dc:creator>Aleksandre Mtchedlishvili</dc:creator>
      <pubDate>Sun, 24 May 2026 11:39:20 +0000</pubDate>
      <link>https://dev.to/aleksandre_mtchedlishvili/pretty-print-isnt-enough-i-built-a-json-xml-viewer-with-a-real-table-view-2b99</link>
      <guid>https://dev.to/aleksandre_mtchedlishvili/pretty-print-isnt-enough-i-built-a-json-xml-viewer-with-a-real-table-view-2b99</guid>
      <description>&lt;p&gt;Every JSON viewer I've tried does the same thing: it pretty-prints. Add indentation, color the keys, fold the brackets. Useful for a 10-line config — useless for the actual data shape developers deal with every day, which is an &lt;strong&gt;array of objects&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A pretty-printed 50-row API response is still 500 lines you scroll top-to-bottom. You can't say &lt;em&gt;"show me only admins"&lt;/em&gt; without re-reading every record. You can't sort by date. You can't search across rows.&lt;/p&gt;

&lt;p&gt;That's not a viewer problem — that's the wrong format. Arrays of objects want to be &lt;strong&gt;tables&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────┬───────┬───────┬────────┐
│ ID │ NAME  │ ROLE  │ ACTIVE │
├────┼───────┼───────┼────────┤
│ 1  │ Alice │ admin │ ● Yes  │
│ 2  │ Bob   │ user  │ ● Yes  │
│ 3  │ Carol │ user  │ ● No   │
│ 4  │ Dave  │ admin │ ● Yes  │
└────┴───────┴───────┴────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sortable columns. Searchable rows. Click any row to expand the full nested detail. Same data — but you &lt;em&gt;scan&lt;/em&gt; it instead of &lt;em&gt;read&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://prettyjsonxml.com" rel="noopener noreferrer"&gt;prettyjsonxml.com&lt;/a&gt; — a JSON and XML viewer that turns arrays into real tables (plus a foldable tree view, format, minify, base64 image preview). One HTML file. No backend. No build step. Runs entirely in the browser; your data never leaves your machine.&lt;/p&gt;

&lt;p&gt;What I didn't expect: making the table view actually fast on a 9 MB API response was a much harder problem than it looked. Here's the unedited story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version: works, then doesn't
&lt;/h2&gt;

&lt;p&gt;V1 was straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;renderTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tbody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... build cells&lt;/span&gt;
    &lt;span class="nx"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beautiful for the 5-row examples I tested. Then I pasted a real API response. &lt;strong&gt;The browser froze for 1.5 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The freeze had two sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;JSON.parse&lt;/code&gt; on 9 MB blocks the main thread for ~500 ms&lt;/li&gt;
&lt;li&gt;Creating 30,000 × 2 rows (main + detail) = 60,000 DOM nodes blocks for another ~1500 ms&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can't make either fast, but you can make them &lt;em&gt;not freeze the UI&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: &lt;code&gt;content-visibility: auto&lt;/code&gt; — the one-line win
&lt;/h2&gt;

&lt;p&gt;Modern browsers will skip layout for off-screen content if you tell them they can. One CSS rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.data-table&lt;/span&gt; &lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="nc"&gt;.row-main&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;content-visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;contain-intrinsic-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="m"&gt;40px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;content-visibility: auto&lt;/code&gt; tells the browser: &lt;em&gt;"don't bother computing layout for this element until it's about to scroll into view."&lt;/em&gt; &lt;code&gt;contain-intrinsic-size&lt;/code&gt; gives it a placeholder height so the scrollbar still represents the full document.&lt;/p&gt;

&lt;p&gt;Total render time didn't change — the work still happens — but &lt;strong&gt;perceived&lt;/strong&gt; performance jumped because the browser is free to paint visible parts first. Criminally underused. Works on ~95% of modern browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Web Workers for &lt;code&gt;JSON.parse&lt;/code&gt; — the counter-intuitive lesson
&lt;/h2&gt;

&lt;p&gt;The next freeze was &lt;code&gt;JSON.parse&lt;/code&gt; itself. Conventional wisdom: move expensive parsing off the main thread with a Web Worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;`
  self.onmessage = (e) =&amp;gt; {
    const parsed = JSON.parse(e.data);
    self.postMessage(parsed);
  };
`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/javascript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;

&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largeJsonString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Main thread stays responsive. Right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It actually felt slower.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's why: when the worker sends the parsed object back via &lt;code&gt;postMessage&lt;/code&gt;, the main thread has to &lt;strong&gt;structured-clone&lt;/strong&gt; the entire object graph to receive it. For a 30,000-object array, that clone is 300–500 ms — &lt;em&gt;also on the main thread&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I'd successfully moved 500 ms of &lt;code&gt;JSON.parse&lt;/code&gt; off the main thread, and added 400 ms of structured-clone onto it. Net win: ~100 ms. And the user perceives the freeze &lt;em&gt;later&lt;/em&gt; in the flow, after they clicked a button expecting an instant result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Workers are for CPU-bound work whose result doesn't have to come back.&lt;/strong&gt; When 95% of the workload is the parsed object itself, structured-clone cost dominates the benefit.&lt;/p&gt;

&lt;p&gt;I kept the worker for Format / Minify (output is a string, cheap to clone). For the parse-then-render flow, it was barely net-positive. The real fix was elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Virtual scrolling — the actual fix
&lt;/h2&gt;

&lt;p&gt;For 30,000-row tables, you don't render 30,000 rows. You render the ~50 the user can see, and swap them as they scroll.&lt;/p&gt;

&lt;p&gt;The gotcha with &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;: you can't &lt;code&gt;position: absolute&lt;/code&gt; rows (table layout doesn't allow it). Instead, use &lt;strong&gt;spacer rows&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height:850px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;   &lt;span class="c"&gt;&amp;lt;!-- spacer for rows above viewport --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;row 21&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;row 22&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    ...
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;row 70&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height:1200px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- spacer for rows below viewport --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On every scroll event, recompute which rows are visible and swap them in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onScroll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tbodyRect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;viewTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tbodyRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;viewBottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;viewTop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;scrollContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewTop&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;ROW_HEIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endRow&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewBottom&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;ROW_HEIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Remove old visible rows, build new ones from items[startRow..endRow]&lt;/span&gt;
  &lt;span class="c1"&gt;// Adjust spacer heights so scrollbar position stays correct&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a 30,000-row JSON array, this goes from "tab completely unresponsive" to "smooth 60 fps scroll." Even at more modest sizes — say a few hundred rows — the win is that &lt;strong&gt;search and sort become instant&lt;/strong&gt; because they now operate on a JavaScript array, not by walking thousands of DOM nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bug I lost 20 minutes to&lt;/strong&gt;: my virtual scroller listened to &lt;code&gt;window.scroll&lt;/code&gt;. But my page had &lt;code&gt;body { overflow: hidden }&lt;/code&gt; and &lt;code&gt;&amp;lt;main&amp;gt; { overflow: auto }&lt;/code&gt; — so &lt;code&gt;window.scroll&lt;/code&gt; &lt;em&gt;never&lt;/em&gt; fired. The actual scroll events came from &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Walk up to find the nearest ancestor that actually scrolls&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findScrollContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;overflowY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;oy&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;oy&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always resolve the scroll container at runtime. Don't assume it's &lt;code&gt;window&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The textarea trap
&lt;/h2&gt;

&lt;p&gt;One more freeze I didn't expect: assigning a 9 MB string to &lt;code&gt;&amp;lt;textarea&amp;gt;.value&lt;/code&gt; blocks the main thread for ~300–500 ms by itself. The browser has to compute layout for the text content even though most of it is below the fold.&lt;/p&gt;

&lt;p&gt;Fix: for files &amp;gt; 5 MB, leave the textarea empty and show a styled "loaded state" panel instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;showLoadedOverlay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data still parses and renders into the viewer — it's just not in the editable textarea. For 9 MB files no one wants to hand-edit anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with virtual scrolling.&lt;/strong&gt; Don't add it as Phase 3. It's the only thing that scales. Everything else is polish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Question the "move it to a Worker" reflex.&lt;/strong&gt; Worker is great for compute you don't need back. Bad for parse-then-clone flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;content-visibility: auto&lt;/code&gt; everywhere it applies.&lt;/strong&gt; It's basically free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with real production data early.&lt;/strong&gt; My 5-row test cases hid every interesting bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The single-file thing
&lt;/h2&gt;

&lt;p&gt;I kept arguing with myself whether to add a build step. "Just bundle it, just split into modules, just add TypeScript…" Every prototype of that was technically cleaner and materially worse — now I had to host more files, worry about cache busting, maintain a toolchain.&lt;/p&gt;

&lt;p&gt;For a tool whose entire pitch is "100% in your browser, no server," shipping it as one HTML file you can &lt;code&gt;Save As&lt;/code&gt; and run offline is the right product decision. Pragma over purity.&lt;/p&gt;

&lt;p&gt;Final: ~225 KB single HTML, no dependencies, no build, served as-is from Cloudflare Pages.&lt;/p&gt;




&lt;p&gt;If you want to try it: &lt;strong&gt;&lt;a href="https://prettyjsonxml.com" rel="noopener noreferrer"&gt;prettyjsonxml.com&lt;/a&gt;&lt;/strong&gt; — paste any JSON or XML, view as a sortable table or foldable tree. Built it because I needed it. Sharing it because maybe you do too.&lt;/p&gt;

&lt;p&gt;What perf tricks have you used for big-data UIs? Always curious about the ones that surprised people.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
