<?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: Aditya Prakash</title>
    <description>The latest articles on DEV Community by Aditya Prakash (@adityaprakash).</description>
    <link>https://dev.to/adityaprakash</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%2F4136422%2F116337d3-4a30-406e-b08f-95b492204b61.png</url>
      <title>DEV Community: Aditya Prakash</title>
      <link>https://dev.to/adityaprakash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adityaprakash"/>
    <language>en</language>
    <item>
      <title>I Benchmarked Five Ways to Speed Up a Slow React Table. Two of Them Did Nothing.</title>
      <dc:creator>Aditya Prakash</dc:creator>
      <pubDate>Mon, 21 Sep 2026 21:24:17 +0000</pubDate>
      <link>https://dev.to/adityaprakash/i-benchmarked-five-ways-to-speed-up-a-slow-react-table-two-of-them-did-nothing-4b0i</link>
      <guid>https://dev.to/adityaprakash/i-benchmarked-five-ways-to-speed-up-a-slow-react-table-two-of-them-did-nothing-4b0i</guid>
      <description>&lt;p&gt;A table of 4,000 rows with a filter box above it. You type "acme" and the cursor lags behind your fingers.&lt;/p&gt;

&lt;p&gt;Every frontend developer hits this shape eventually, and the advice for fixing it is remarkably consistent: wrap the row in &lt;code&gt;React.memo&lt;/code&gt;. That advice is where I started too, and it turns out to be close to worthless on its own.&lt;/p&gt;

&lt;p&gt;So rather than argue about it, I built a harness that measures five different fixes on the same data and reports what each one is actually worth. &lt;a href="https://github.com/Adityapk1234/react-rerender-benchmark" rel="noopener noreferrer"&gt;The code is on GitHub&lt;/a&gt; — there's a prebuilt single-file version you can open in a browser and get your own numbers in about thirty seconds.&lt;/p&gt;

&lt;p&gt;Here's what it found.&lt;/p&gt;




&lt;h2&gt;
  
  
  The results first
&lt;/h2&gt;

&lt;p&gt;Production build, 4,000 rows, median of two passes, measuring the slowest keystroke:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Longest re-render&lt;/th&gt;
&lt;th&gt;Does it still filter?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Naive&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;166 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. &lt;code&gt;memo()&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;153 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. &lt;code&gt;memo&lt;/code&gt; + &lt;code&gt;useCallback&lt;/code&gt; + &lt;code&gt;useMemo&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;47 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. &lt;code&gt;children&lt;/code&gt; bailout&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.4 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no — see below&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Virtualized&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.6 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Measured on a 2-core Linux VM against a production build. Your absolute numbers will differ — mine did by 10–15% between runs. The ratios are the point, and those were stable across six runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things jump out, and the second one is a trap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;memo&lt;/code&gt; on its own bought 13ms out of 166.&lt;/strong&gt; Within noise of doing nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variant 4 looks like the winner and isn't.&lt;/strong&gt; It's 0.4ms because it stops doing the work, not because it does the work faster. More on that below, because it's the most interesting result here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why &lt;code&gt;memo&lt;/code&gt; alone does nothing
&lt;/h2&gt;

&lt;p&gt;Here's variant 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;InvoiceRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memo&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;InvoiceRow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSelect&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;onClick&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;formatMoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&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;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Correct on its own terms. But look at what the parent hands it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InvoiceRow&lt;/span&gt;
  &lt;span class="na"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onSelect&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="nx"&gt;id&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;openDrawer&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="si"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// ← new function object, every render&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;&lt;code&gt;memo&lt;/code&gt; does a &lt;strong&gt;shallow&lt;/strong&gt; comparison of props. That arrow function is freshly created on every render of the parent, so &lt;code&gt;prevProps.onSelect === nextProps.onSelect&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;memo&lt;/code&gt; concludes the props changed, and it re-renders. Every time. For every row.&lt;/p&gt;

&lt;p&gt;The same thing happens with inline objects (&lt;code&gt;style={{ padding: 8 }}&lt;/code&gt;) and inline arrays (&lt;code&gt;columns={[a, b]}&lt;/code&gt;). And with the filtered array itself — &lt;code&gt;invoices.filter(...)&lt;/code&gt; computed during render is a new array reference every time, so a memoized table can never bail out either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;memo&lt;/code&gt; on a component whose parent passes inline callbacks is decoration.&lt;/strong&gt; You pay for a comparison on every render and buy nothing. The 13ms it appeared to save is measurement noise; in one of my six runs it came out &lt;em&gt;slower&lt;/em&gt; than the naive version.&lt;/p&gt;

&lt;p&gt;Variant 3 fixes it properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&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;handleSelect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setSelected&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;166ms → 47ms. A 3.5x improvement, and the first thing on this list that actually works.&lt;/p&gt;

&lt;p&gt;Two details that are easy to get wrong. &lt;code&gt;handleSelect&lt;/code&gt; takes the id as an &lt;strong&gt;argument&lt;/strong&gt; rather than closing over it — write &lt;code&gt;useCallback(() =&amp;gt; setSelected(inv.id), [inv.id])&lt;/code&gt; inside the &lt;code&gt;.map()&lt;/code&gt; and you've created a fresh function per row per render, which puts you straight back at variant 2. And the empty dependency array is honest rather than a trick to quiet the linter: React guarantees &lt;code&gt;setSelected&lt;/code&gt; is stable.&lt;/p&gt;

&lt;p&gt;Worth saying plainly: that &lt;code&gt;useMemo&lt;/code&gt; around the filter is there to stabilise a &lt;strong&gt;reference&lt;/strong&gt;, not to skip a slow computation. Filtering 4,000 small objects takes well under a millisecond — memoizing it costs more than doing it. If you can't say which of those two reasons applies to a &lt;code&gt;useMemo&lt;/code&gt; you're writing, delete it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Variant 4: the fastest result, and why you can't have it
&lt;/h2&gt;

&lt;p&gt;This is the one I'd flag to anyone quoting benchmarks like these.&lt;/p&gt;

&lt;p&gt;A component's &lt;code&gt;children&lt;/code&gt; prop is constructed by whoever &lt;strong&gt;writes&lt;/strong&gt; the JSX, not by the component that renders it. So if &lt;code&gt;children&lt;/code&gt; is created in a parent that isn't re-rendering, that element object keeps the same reference, React compares it, sees it's identical, and skips the entire subtree. No &lt;code&gt;memo&lt;/code&gt; anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;InvoicesPage&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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;loadInvoices&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchShell&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InvoiceTable&lt;/span&gt; &lt;span class="na"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;   &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* created here */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SearchShell&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&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;SearchShell&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// changes here&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&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="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;setQuery&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;0.4ms, because the table genuinely does not re-render at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the table can no longer see &lt;code&gt;query&lt;/code&gt;.&lt;/strong&gt; That's not a detail I glossed over in the benchmark — it's inherent to the pattern. The value that changes lives in &lt;code&gt;SearchShell&lt;/code&gt;, and &lt;code&gt;children&lt;/code&gt; was built outside it. So variant 4 isn't a faster filter. It's no filter.&lt;/p&gt;

&lt;p&gt;I left it in the benchmark anyway, because the pattern is genuinely valuable for the large category of state that &lt;em&gt;doesn't&lt;/em&gt; need to reach the expensive subtree: a drawer being open, a hovered row, a collapsed sidebar, an unsaved form draft. For all of those, this deletes the problem instead of managing it, and it can't be silently broken later the way a &lt;code&gt;memo&lt;/code&gt; chain can.&lt;/p&gt;

&lt;p&gt;For filtering, you need variant 3 or variant 5.&lt;/p&gt;




&lt;h2&gt;
  
  
  Variant 5: stop rendering rows nobody can see
&lt;/h2&gt;

&lt;p&gt;Everything above reduces &lt;em&gt;how often&lt;/em&gt; you render 4,000 rows. This reduces the 4,000.&lt;/p&gt;

&lt;p&gt;The viewport fits about 30 rows. The other 3,970 are DOM nodes that exist so the scrollbar is the right height. Virtualization renders the visible window plus a small buffer and translates it as you scroll.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useVirtualizer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-virtual&lt;/span&gt;&lt;span class="dl"&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;virtualizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useVirtualizer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;visible&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="na"&gt;getScrollElement&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="nx"&gt;parentRef&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="na"&gt;estimateSize&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="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;overscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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;166ms → 3.6ms, and it still filters. In the harness, 23 rows exist in the DOM instead of 4,000.&lt;/p&gt;

&lt;p&gt;The costs are real and rarely mentioned. Ctrl+F stops finding off-screen content. Keyboard navigation and screen readers need deliberate work. Variable-height rows need measurement rather than estimation. Your print and CSV-export paths need a separate non-virtualized render, and you will forget this and hear about it from a user. Virtualizing real &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; rows also needs CSS grid or &lt;code&gt;table-layout: fixed&lt;/code&gt; — my harness uses divs for exactly this reason.&lt;/p&gt;

&lt;p&gt;Below about a thousand rows it's usually not worth the complexity. Above a few thousand it's the only thing that really works.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the measurement works
&lt;/h2&gt;

&lt;p&gt;No DevTools, because React's &lt;code&gt;&amp;lt;Profiler&amp;gt;&lt;/code&gt; is compiled out of production builds and I wanted production numbers. &lt;code&gt;input&lt;/code&gt; is a discrete event, so React flushes the re-render synchronously inside &lt;code&gt;dispatchEvent&lt;/code&gt; — which means wall time around the dispatch is the render plus commit cost:&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;timedKeystroke&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="nx"&gt;value&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;setter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOwnPropertyDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;set&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;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;setter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&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="nx"&gt;value&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="nf"&gt;dispatchEvent&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;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&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="na"&gt;bubbles&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&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;It types &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;ac&lt;/code&gt;, &lt;code&gt;acm&lt;/code&gt;, &lt;code&gt;acme&lt;/code&gt;, keeps the slowest of the four, and takes the median of two passes. &lt;code&gt;StrictMode&lt;/code&gt; is off — it double-renders and would inflate everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd actually do, in order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure before changing anything.&lt;/strong&gt; React DevTools → Components → "Highlight updates when components render", then type one character. If the whole table flashes, you're done diagnosing in four seconds. Write the number down; "it feels laggy" isn't something you can improve against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move state down&lt;/strong&gt; to the smallest component that needs it. Free, and nothing can silently undo it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass expensive subtrees as &lt;code&gt;children&lt;/code&gt;&lt;/strong&gt; where the value doesn't need to reach them. Bailouts with no &lt;code&gt;memo&lt;/code&gt; to break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then&lt;/strong&gt; &lt;code&gt;memo&lt;/code&gt; — with &lt;em&gt;every&lt;/em&gt; object, array and function prop stabilised, or genuinely don't bother.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtualize&lt;/strong&gt; once the list is long enough to earn the accessibility tax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure again, and revert anything that didn't move the number.&lt;/strong&gt; Memoization you can't justify isn't an optimization, it's maintenance debt with a good reputation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most advice starts at step four, because that's the step with the famous API. Steps two and three have no API at all, which is why they're the ones that hold.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;The harness is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/Adityapk1234/react-rerender-benchmark" rel="noopener noreferrer"&gt;Adityapk1234/react-rerender-benchmark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The quickest path is &lt;code&gt;react-bench.html&lt;/code&gt; in that repo — a self-contained build with no install step. Open it and it measures your machine. From source it is &lt;code&gt;npm install &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm run preview&lt;/code&gt;; use &lt;code&gt;build&lt;/code&gt; + &lt;code&gt;preview&lt;/code&gt; rather than &lt;code&gt;dev&lt;/code&gt;, because development builds are much slower and &lt;code&gt;StrictMode&lt;/code&gt; double-renders, so &lt;code&gt;dev&lt;/code&gt; numbers are not numbers you can publish.&lt;/p&gt;

&lt;p&gt;Numbers on your hardware will differ from mine. The one I'd most like corroborated is the gap between variant 1 and variant 2 — on my setup it was noise across six runs, and if it isn't on yours I want to know why.&lt;/p&gt;

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