<?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: EliteGrid</title>
    <description>The latest articles on DEV Community by EliteGrid (@elitegrid).</description>
    <link>https://dev.to/elitegrid</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3952895%2F706899db-70ea-4293-bf38-eada4868f761.png</url>
      <title>DEV Community: EliteGrid</title>
      <link>https://dev.to/elitegrid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elitegrid"/>
    <language>en</language>
    <item>
      <title>How We Render 1 Million Rows Without Breaking a Sweat — Inside EliteGrid's Virtual Scrolling Engine</title>
      <dc:creator>EliteGrid</dc:creator>
      <pubDate>Thu, 18 Jun 2026 02:39:37 +0000</pubDate>
      <link>https://dev.to/elitegrid/how-we-render-1-million-rows-without-breaking-a-sweat-inside-elitegrids-virtual-scrolling-engine-4j79</link>
      <guid>https://dev.to/elitegrid/how-we-render-1-million-rows-without-breaking-a-sweat-inside-elitegrids-virtual-scrolling-engine-4j79</guid>
      <description>&lt;p&gt;&lt;em&gt;A deep dive into the architecture decisions, bugs, and breakthroughs behind building a high-performance data grid from scratch.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;When I started building &lt;a href="https://elitegrid.dev" rel="noopener noreferrer"&gt;EliteGrid&lt;/a&gt; — a TypeScript data grid library for React and Vue — I knew virtual scrolling would be the hardest part to get right. Not because the concept is complex, but because the &lt;em&gt;details&lt;/em&gt; are brutal.&lt;/p&gt;

&lt;p&gt;This is the story of how we built it, what broke along the way, and what the final architecture looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Virtual Scrolling (and Why Does It Matter)?
&lt;/h2&gt;

&lt;p&gt;The naive approach to rendering a data grid is simple: loop over your data and render a row for each item. For 100 rows, this works fine. For 10,000 rows, your browser starts to sweat. For 1,000,000 rows, the page freezes entirely.&lt;/p&gt;

&lt;p&gt;The DOM is expensive. Each row is a collection of elements, event listeners, and style computations. Browsers aren't designed to handle hundreds of thousands of DOM nodes simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual scrolling solves this by only rendering the rows currently visible on screen&lt;/strong&gt; — plus a small buffer above and below. As the user scrolls, rows are recycled: off-screen rows are removed and new ones are inserted in their place.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;1,000,000 rows in the dataset, but only ~30 rows in the DOM at any time.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Basic Architecture
&lt;/h2&gt;

&lt;p&gt;At its core, virtual scrolling needs three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A scrollable container&lt;/strong&gt; with a fixed height&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A spacer element&lt;/strong&gt; whose height equals &lt;code&gt;totalRows × rowHeight&lt;/code&gt; — this creates the illusion of a full list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A visible window&lt;/strong&gt; that calculates which rows to render based on &lt;code&gt;scrollTop&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateVisibleRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;containerHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rowHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;totalRows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bufferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;startIndex&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;scrollTop&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;rowHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;bufferSize&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;visibleCount&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;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;containerHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;rowHeight&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;endIndex&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;totalRows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;visibleCount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bufferSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endIndex&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;Simple enough. But this is where "simple" ends.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #1: The Blank Screen on Fast Scroll
&lt;/h2&gt;

&lt;p&gt;Our first implementation worked perfectly at normal scroll speeds. But when users scrolled fast — flicking the trackpad or dragging the scrollbar — they'd see a &lt;strong&gt;blank white screen&lt;/strong&gt; for a split second before rows appeared.&lt;/p&gt;

&lt;p&gt;The cause took us a while to find.&lt;/p&gt;

&lt;p&gt;We were using &lt;code&gt;requestAnimationFrame&lt;/code&gt; (rAF) to batch DOM updates and avoid layout thrashing. The scroll handler looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ BROKEN&lt;/span&gt;
&lt;span class="nx"&gt;containerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&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;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// captured here&lt;/span&gt;

  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;updateVisibleRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// used here — but scrollTop is already stale!&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 problem: &lt;strong&gt;by the time the rAF callback fired, the user had already scrolled further.&lt;/strong&gt; We were rendering rows for a scroll position that no longer existed.&lt;/p&gt;

&lt;p&gt;The fix was simple but non-obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ FIXED&lt;/span&gt;
&lt;span class="nx"&gt;containerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&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;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// read INSIDE the rAF&lt;/span&gt;
    &lt;span class="nf"&gt;updateVisibleRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&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;By reading &lt;code&gt;scrollTop&lt;/code&gt; inside the rAF callback instead of the scroll event, we always get the &lt;em&gt;current&lt;/em&gt; scroll position at paint time. Blank screen gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #2: O(n²) Row ID Lookups Killing Performance
&lt;/h2&gt;

&lt;p&gt;EliteGrid's plugin system needs to map between rendered rows and their data source IDs constantly — for selection, editing, focus management, and export.&lt;/p&gt;

&lt;p&gt;Our original approach was a linear scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ O(n) lookup, called hundreds of times per scroll&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRowId&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="nx"&gt;RowData&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&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;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&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;data&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;id&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With 10,000 rows and plugins calling this on every scroll event, we were doing &lt;strong&gt;millions of comparisons per second&lt;/strong&gt;. The grid would visibly lag on mid-range hardware.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;code&gt;WeakMap&lt;/code&gt; reverse index built alongside the row model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ O(1) lookup&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RowModelPlugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;dataToId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;WeakMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RowData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;setRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataToId&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;WeakMap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for &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;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataToId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&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="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;span class="nf"&gt;getRowId&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="nx"&gt;RowData&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataToId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&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;&lt;code&gt;WeakMap&lt;/code&gt; is perfect here — it doesn't prevent garbage collection of row data objects when rows are recycled, and lookups are O(1). Scroll performance went from janky to buttery smooth.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plugin Architecture That Makes It All Work
&lt;/h2&gt;

&lt;p&gt;One thing that makes EliteGrid different from other grids is its &lt;strong&gt;microkernel + plugin architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The core grid kernel does almost nothing by itself. It manages a central event bus and a plugin registry. Every feature — virtual scrolling, selection, editing, sorting, export — is a plugin that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Registers with the kernel on mount&lt;/li&gt;
&lt;li&gt;Subscribes to events it cares about&lt;/li&gt;
&lt;li&gt;Emits events when state changes
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified plugin interface&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GridPlugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GridKernel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Example: ScrollPlugin subscribes to scroll events&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScrollPlugin&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;GridPlugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&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="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GridKernel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&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="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="p"&gt;})&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endIndex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateVisibleRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
      &lt;span class="nx"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VISIBLE_ROWS_CHANGED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endIndex&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;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// cleanup&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;This means the virtual scrolling engine doesn't know anything about selection or editing — and vice versa. Plugins are testable in isolation, and adding new features doesn't touch existing code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Numbers
&lt;/h2&gt;

&lt;p&gt;Running our playground at &lt;a href="https://elitegrid.dev/playground" rel="noopener noreferrer"&gt;elitegrid.dev/playground&lt;/a&gt; with different dataset sizes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset Size&lt;/th&gt;
&lt;th&gt;DOM Nodes&lt;/th&gt;
&lt;th&gt;Initial Render&lt;/th&gt;
&lt;th&gt;Scroll FPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000 rows&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;&amp;lt; 50ms&lt;/td&gt;
&lt;td&gt;60fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000 rows&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;&amp;lt; 80ms&lt;/td&gt;
&lt;td&gt;60fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000 rows&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;&amp;lt; 100ms&lt;/td&gt;
&lt;td&gt;60fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000 rows&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;&amp;lt; 120ms&lt;/td&gt;
&lt;td&gt;60fps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The DOM node count stays constant regardless of dataset size. That's the whole point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;EliteGrid is launching on npm at the end of June with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Vue 3 and JavaScript adapters&lt;/li&gt;
&lt;li&gt;Full TypeScript types&lt;/li&gt;
&lt;li&gt;Virtual scrolling, inline editing, sorting, filtering, CSV export
If you want to try it before launch, the playground is live at &lt;strong&gt;&lt;a href="https://elitegrid.dev/playground" rel="noopener noreferrer"&gt;elitegrid.dev/playground&lt;/a&gt;&lt;/strong&gt; — no install needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you're building something with large datasets and want to give feedback, I'd genuinely love to hear from you. Drop a comment below or find me on Twitter at &lt;a href="https://x.com/elitegridhq" rel="noopener noreferrer"&gt;@elitegridhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>vue</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We launched an interactive playground for EliteGrid — try it in your browser</title>
      <dc:creator>EliteGrid</dc:creator>
      <pubDate>Sat, 06 Jun 2026 17:05:24 +0000</pubDate>
      <link>https://dev.to/elitegrid/we-launched-an-interactive-playground-for-elitegrid-try-it-in-your-browser-1eh9</link>
      <guid>https://dev.to/elitegrid/we-launched-an-interactive-playground-for-elitegrid-try-it-in-your-browser-1eh9</guid>
      <description>&lt;p&gt;Today the EliteGrid playground went live at &lt;strong&gt;elitegrid.dev/playground&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;EliteGrid is a TypeScript data grid library with React and Vue adapters. We wanted developers to be able to &lt;em&gt;feel&lt;/em&gt; it before installing anything, so We built a playground with a real code editor and live preview side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a playground?
&lt;/h2&gt;

&lt;p&gt;Data grid libraries are hard to evaluate from docs alone. You need to see how it handles large datasets, how the API reads when you write it yourself, how editing and validation feel in practice. A README can't show you that.&lt;/p&gt;

&lt;p&gt;So the playground came before the npm publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Virtual Scroll — 10K Rows&lt;/strong&gt;&lt;br&gt;
10,000 rows rendered with virtual scrolling. Scroll fast, scroll slow — see how it holds up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real API — Live Fetch&lt;/strong&gt;&lt;br&gt;
Grid wired to a real API endpoint. Shows how async data sources work in EliteGrid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline Edit — Validated&lt;/strong&gt;&lt;br&gt;
Edit cells inline with validation rules. Trigger an error state, see how it surfaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSV Export — Multi-Scope&lt;/strong&gt;&lt;br&gt;
Export selected rows, current page, or the full dataset. Three modes, one API call.&lt;/p&gt;

&lt;p&gt;Switch between React and Vue from the top nav — same config API, both adapters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm looking for
&lt;/h2&gt;

&lt;p&gt;Honest feedback. What's missing? What feels awkward in the API? What scenario do you hit in production that isn't covered here?&lt;/p&gt;

&lt;p&gt;Try it at &lt;strong&gt;elitegrid.dev/playground&lt;/strong&gt; and drop a comment.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>vue</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I'm building a TypeScript data grid where config reads like English</title>
      <dc:creator>EliteGrid</dc:creator>
      <pubDate>Tue, 26 May 2026 17:15:01 +0000</pubDate>
      <link>https://dev.to/elitegridhq/im-building-a-typescript-data-grid-where-config-reads-like-english-5b14</link>
      <guid>https://dev.to/elitegridhq/im-building-a-typescript-data-grid-where-config-reads-like-english-5b14</guid>
      <description>&lt;p&gt;I've integrated a lot of data grids over the years.&lt;/p&gt;

&lt;p&gt;Every time, the experience was the same — open the docs, search for the &lt;br&gt;
prop I need, find three different ways to do the same thing, pick one, &lt;br&gt;
get it wrong, Google it, eventually get it working, and forget how I did &lt;br&gt;
it by the next project.&lt;/p&gt;

&lt;p&gt;I'm a developer. I shouldn't need tribal knowledge to configure a UI component.&lt;/p&gt;

&lt;p&gt;So I started building EliteGrid.&lt;/p&gt;
&lt;h2&gt;
  
  
  The core idea: grouped config API
&lt;/h2&gt;

&lt;p&gt;Most grids dump every option at the top level. Hundreds of flat props with &lt;br&gt;
no consistent naming pattern. You end up memorizing prop names instead of &lt;br&gt;
understanding a mental model.&lt;/p&gt;

&lt;p&gt;EliteGrid uses a grouped, namespaced config API. Every feature has its own &lt;br&gt;
namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EliteGrid&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;sorting&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;multi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;filtering&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;editing&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;doubleClick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;appearance&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;density&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Config that reads like English. TypeScript autocompletes every property. &lt;br&gt;
You never need to guess a prop name.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;EliteGrid is built on a microkernel with 13 independent plugins communicating &lt;br&gt;
through a shared event bus and data store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DataSource&lt;/strong&gt; — raw data ingestion and normalization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RowModel&lt;/strong&gt; — row lifecycle, identity, and memoization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ColumnModel&lt;/strong&gt; — column definitions, width, visibility, pinning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt; — single and multi-column sort with custom comparators&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt; — text, number, date, and boolean filters per column&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination&lt;/strong&gt; — client-side and server-side pagination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selection&lt;/strong&gt; — single and multi-select with keyboard support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit&lt;/strong&gt; — inline editing with sync/async validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Viewport&lt;/strong&gt; — visible row calculation and scroll management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt; — CSV export with custom formatters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each plugin is completely independent. No circular dependencies. &lt;br&gt;
No hidden coupling. Zero runtime dependencies on the core.&lt;/p&gt;

&lt;p&gt;We currently have 662 passing tests across the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility by default
&lt;/h2&gt;

&lt;p&gt;Full WCAG 2.1 AA compliance ships as part of the default experience. &lt;br&gt;
Zero configuration required.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full ARIA Grid Pattern implementation&lt;/li&gt;
&lt;li&gt;Screen reader announcements for all state changes&lt;/li&gt;
&lt;li&gt;Complete keyboard navigation — Arrow keys, Home, End, PageUp, PageDown&lt;/li&gt;
&lt;li&gt;Roving tabindex focus management&lt;/li&gt;
&lt;li&gt;prefers-reduced-motion respected&lt;/li&gt;
&lt;li&gt;Windows High Contrast mode supported&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every user deserves a great grid, not just sighted mouse users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docs as a first-class feature
&lt;/h2&gt;

&lt;p&gt;The goal: a junior developer should be able to integrate EliteGrid &lt;br&gt;
from the docs alone — no AI, no Stack Overflow, no tribal knowledge.&lt;/p&gt;

&lt;p&gt;That's a high bar. We're treating it as a product requirement, not &lt;br&gt;
an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we are
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Core engine — 13 plugins, 662 passing tests&lt;/li&gt;
&lt;li&gt;✅ Grouped config API&lt;/li&gt;
&lt;li&gt;🔨 React and Vue adapters — building now&lt;/li&gt;
&lt;li&gt;🔨 Interactive sandbox on StackBlitz — building now&lt;/li&gt;
&lt;li&gt;⏭ Public npm launch — coming soon&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Community version will be free and open source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow along
&lt;/h2&gt;

&lt;p&gt;If this sounds like the grid you've been waiting for, join the waitlist at &lt;a href="https://elitegrid.dev" rel="noopener noreferrer"&gt;elitegrid.dev&lt;/a&gt; — one email when the sandbox is ready, one at launch.&lt;/p&gt;

&lt;p&gt;I'd love to hear what you think in the comments. What matters most to you in a data grid?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
