<?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: anand mohan</title>
    <description>The latest articles on DEV Community by anand mohan (@anand_mohan_f798b1270e7fb).</description>
    <link>https://dev.to/anand_mohan_f798b1270e7fb</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%2F1934866%2F077be3e0-b08a-43f2-b452-37db1ad76f53.jpg</url>
      <title>DEV Community: anand mohan</title>
      <link>https://dev.to/anand_mohan_f798b1270e7fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anand_mohan_f798b1270e7fb"/>
    <language>en</language>
    <item>
      <title>You Probably Don't Need Virtualization (But Here's How It Actually Works)</title>
      <dc:creator>anand mohan</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:13:51 +0000</pubDate>
      <link>https://dev.to/anand_mohan_f798b1270e7fb/you-probably-dont-need-virtualization-but-heres-how-it-actually-works-4141</link>
      <guid>https://dev.to/anand_mohan_f798b1270e7fb/you-probably-dont-need-virtualization-but-heres-how-it-actually-works-4141</guid>
      <description>&lt;p&gt;"Virtualization" might be the most buzzed, least understood word in frontend performance.&lt;/p&gt;

&lt;p&gt;Every optimization discussion, someone drops it. My roommate does it every time React or Angular perfmonce comes up: "just virtualize it." So I asked him one follow-up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Okay — what happens to the rows that scrolled away?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Silence. He'd never thought about it. Neither had I, honestly, until I added virtual scrolling to a lead table this week and started pulling the thread. "It only renders what fits on screen" is the marketing sentence — true, but it's maybe 20% of the mechanism. The interesting 80% is in the follow-up questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens to rows that scroll out of view?&lt;/li&gt;
&lt;li&gt;If most rows don't exist, why doesn't the scrollbar shrink?&lt;/li&gt;
&lt;li&gt;If new rows are created on every scroll frame, why doesn't that lag?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post answers all three, covers the two things I had completely wrong, and shows how to implement it in both Angular and React.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, two patterns people mix up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Infinite scrolling&lt;/strong&gt; is a &lt;em&gt;data loading&lt;/em&gt; pattern. Instead of pagination, you fetch the next page when the user nears the bottom. The DOM keeps growing — load 5,000 items, get 5,000 nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtualization&lt;/strong&gt; is a &lt;em&gt;rendering&lt;/em&gt; pattern. Only the rows visible in the viewport (plus a small buffer) exist in the DOM. Everything else is just data in a JavaScript array.&lt;/p&gt;

&lt;p&gt;They solve different problems, and they pair together. Infinite scroll without virtualization dies at scale — a list that infinite-scrolls to 2,000 rendered rows will lag hard. Gmail, Twitter, every big feed you use combines both: virtualized list + fetch next page when the scroller nears the end of loaded data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Confusion #1: "itemSize=56 means 56 rows are rendered"
&lt;/h2&gt;

&lt;p&gt;Nope. I misread this at first, so let me save you the trouble.&lt;/p&gt;

&lt;p&gt;In Angular CDK's virtual scroll, &lt;code&gt;itemSize="56"&lt;/code&gt; is the &lt;strong&gt;pixel height of each row&lt;/strong&gt;, not a count. The number of rows actually in the DOM is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows in DOM ≈ (viewport height / itemSize) + buffer
            ≈ (600px / 56px) + ~5
            ≈ 15–16 rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a 600px-tall list showing 5,000 leads has roughly &lt;strong&gt;15 divs&lt;/strong&gt; in the DOM. Total.&lt;/p&gt;




&lt;h2&gt;
  
  
  Confusion #2: "So the other rows are display:none or deleted, right?"
&lt;/h2&gt;

&lt;p&gt;This was my actual mental model, and it's wrong.&lt;/p&gt;

&lt;p&gt;The off-screen rows are not hidden nor delted. They are &lt;strong&gt;not in the DOM at all&lt;/strong&gt;. No element, no CSS, nothing to hide. They exist only as plain objects in your array.&lt;/p&gt;

&lt;p&gt;Here's what the DOM actually looks like if you inspect a CDK virtual scroll viewport:&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;cdk-virtual-scroll-viewport&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- spacer: fakes total scrollable height, has NO children --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cdk-virtual-scroll-spacer"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"height: 280000px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- the ONLY rendered content, shifted to the right position --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cdk-virtual-scroll-content-wrapper"&lt;/span&gt;
       &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"transform: translateY(137088px)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Lead 2448&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Lead 2449&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ~15 rows. Nothing above, nothing below. --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/cdk-virtual-scroll-viewport&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three tricks make the illusion work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The spacer.&lt;/strong&gt; An invisible element with &lt;code&gt;height = totalItems × itemSize&lt;/code&gt; (5,000 × 56px = 280,000px). This gives the browser a real scrollbar with correct proportions, so it &lt;em&gt;looks&lt;/em&gt; like all 5,000 rows exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The transform.&lt;/strong&gt; On every scroll, the library computes which slice of the array should be visible and &lt;code&gt;translateY&lt;/code&gt;s the content wrapper to exactly that offset. The ~15 divs always sit under your eyes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The math.&lt;/strong&gt; &lt;code&gt;firstVisibleIndex = scrollTop / itemSize&lt;/code&gt;. This is why the fixed &lt;code&gt;itemSize&lt;/code&gt; must be accurate — it's how the library knows which items map to the current scroll position without measuring anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why not &lt;code&gt;display: none&lt;/code&gt;? Because 5,000 hidden divs are still 5,000 DOM nodes — memory allocated, style recalcs, a bigger tree for the browser to manage. &lt;strong&gt;Hidden is not free. Nonexistent is free.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that finally made it click: recycling
&lt;/h2&gt;

&lt;p&gt;Here's the detail that surprised me most. When "Lead 2448" scrolls out of the viewport, its &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; is &lt;strong&gt;not destroyed&lt;/strong&gt;. It gets moved to the other end and its content is swapped to "Lead 2464". The same ~15 divs live forever, just getting new data.&lt;/p&gt;

&lt;p&gt;Open DevTools, inspect the list, scroll fast. You'll see the same divs staying put in the element tree while their text content flips in place. Node count never changes.&lt;/p&gt;

&lt;p&gt;In short: &lt;strong&gt;the structure is reused, only the bindings are refreshed.&lt;/strong&gt; Creating and destroying DOM nodes is the expensive part, so the library keeps a small fixed set of "row slots" and just repaints their contents.&lt;/p&gt;

&lt;p&gt;This isn't new, by the way. Android called it &lt;code&gt;RecyclerView&lt;/code&gt;. iOS calls it &lt;code&gt;dequeueReusableCell&lt;/code&gt;. Web frameworks reinvented one of the oldest UI performance patterns there is.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Angular does it: the template cache
&lt;/h3&gt;

&lt;p&gt;Angular's recycling is &lt;strong&gt;explicit&lt;/strong&gt;. Every row stamped out by &lt;code&gt;*cdkVirtualFor&lt;/code&gt; is an &lt;code&gt;EmbeddedViewRef&lt;/code&gt; — think of it as a clipboard with a printed form: DOM nodes already built, binding slots already wired. Manufacturing one is expensive.&lt;/p&gt;

&lt;p&gt;The template cache is a stack of used clipboards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Row scrolls out → CDK &lt;strong&gt;detaches&lt;/strong&gt; the view from the DOM and parks it in a cache array. The old data is still written on it. Nobody cares — it's not visible anywhere.&lt;/li&gt;
&lt;li&gt;New row scrolls in → CDK &lt;strong&gt;pops a view from the cache&lt;/strong&gt;, overwrites the context (&lt;code&gt;view.context.$implicit = newLead&lt;/code&gt; — literally reassigning what &lt;code&gt;let lead of&lt;/code&gt; points to), and reattaches it. Change detection runs, the text flips.&lt;/li&gt;
&lt;li&gt;Only if the cache is empty does CDK manufacture a new view. That happens on initial render and basically never again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can even control the pool size:&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;div&lt;/span&gt; &lt;span class="na"&gt;*cdkVirtualFor=&lt;/span&gt;&lt;span class="s"&gt;"let lead of leads; templateCacheSize: 20"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set it to &lt;code&gt;0&lt;/code&gt; and CDK destroys/recreates views instead — scrolling gets measurably jankier. A nice way to &lt;em&gt;feel&lt;/em&gt; what the cache buys you.&lt;/p&gt;

&lt;h3&gt;
  
  
  How React does it: reconciliation + keys
&lt;/h3&gt;

&lt;p&gt;React libraries (&lt;code&gt;react-window&lt;/code&gt;, TanStack Virtual) don't keep an explicit pool. Each render they return just the visible slice — &lt;code&gt;items.slice(start, end)&lt;/code&gt; — and lean on React's diffing:&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="c1"&gt;// Position-based key → DOM node reuse (recycling-like behavior)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;visibleItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;getItemStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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 index-style keys, React sees "same key, same position, props changed" and mutates the existing DOM node in place. Effectively recycling — done implicitly by the reconciler instead of an explicit cache.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Angular CDK&lt;/th&gt;
&lt;th&gt;React (react-window)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recycling&lt;/td&gt;
&lt;td&gt;Explicit view pool (&lt;code&gt;templateCacheSize&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Implicit, via reconciliation + keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positioning&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;translateY&lt;/code&gt; on a wrapper&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;position: absolute&lt;/code&gt; per row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle gotcha&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ngOnInit&lt;/code&gt; doesn't re-fire on recycled rows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useEffect([])&lt;/code&gt; doesn't re-fire if the node is reused&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That lifecycle gotcha is a real bug source: recycled rows keep their component instance, so setup done in &lt;code&gt;ngOnInit&lt;/code&gt; based on &lt;code&gt;@Input&lt;/code&gt; shows stale behavior with fresh data. Use &lt;code&gt;ngOnChanges&lt;/code&gt; for anything that depends on the row's data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation: Angular
&lt;/h2&gt;

&lt;p&gt;Virtualization + infinite scroll combined, using &lt;code&gt;@angular/cdk&lt;/code&gt;:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ViewChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&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;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ScrollingModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CdkVirtualScrollViewport&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;@angular/cdk/scrolling&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&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;rxjs&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="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-lead-list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;standalone&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="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ScrollingModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;cdk-virtual-scroll-viewport
      #viewport
      itemSize="56"
      (scrolledIndexChanged)="onScroll()"
      style="height: 600px"
    &amp;gt;
      &amp;lt;div *cdkVirtualFor="let lead of leads$ | async; trackBy: trackById"
           class="row"&amp;gt;
        {{ lead.name }}
      &amp;lt;/div&amp;gt;
    &amp;lt;/cdk-virtual-scroll-viewport&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeadListComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ViewChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewport&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CdkVirtualScrollViewport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;leads$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Lead&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="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;onScroll&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="k"&gt;if &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;loading&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&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;viewport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRenderedRange&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;end&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;total&lt;/span&gt; &lt;span class="o"&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;leads$&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;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// near the end of loaded data → fetch next page&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;end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadNextPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;loadNextPage&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLeads&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;page&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newLeads&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// new array, not push() — OnPush needs a new reference&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;leads$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;leads$&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="nx"&gt;newLeads&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;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;trackById&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&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;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lead&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;lead&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;Things that will bite you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The viewport needs an explicit height&lt;/strong&gt; (fixed px or &lt;code&gt;flex: 1&lt;/code&gt; in a flex parent). Zero-height viewport = nothing renders. Classic first-time bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;itemSize&lt;/code&gt; must match your CSS row height exactly.&lt;/strong&gt; The scroll math depends on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;trackBy&lt;/code&gt; is mandatory&lt;/strong&gt;, not optional — without it, recycled rows re-render fully and you lose half the benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variable row heights are the dealbreaker.&lt;/strong&gt; The &lt;code&gt;autosize&lt;/code&gt; strategy exists in &lt;code&gt;@angular/cdk-experimental&lt;/code&gt; but it's flaky. Design rows to a fixed height if you can.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation: React
&lt;/h2&gt;

&lt;p&gt;Same pattern with &lt;code&gt;react-window&lt;/code&gt;:&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;FixedSizeList&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;react-window&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useCallback&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;react&lt;/span&gt;&lt;span class="dl"&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;LeadList&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;leads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLeads&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&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="kc"&gt;false&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;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPage&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="mi"&gt;0&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;loadMore&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="k"&gt;async &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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loading&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="nf"&gt;setLoading&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLeads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setLeads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nf"&gt;setPage&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;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&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="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&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;FixedSizeList&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;itemCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;leads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;itemSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
      &lt;span class="na"&gt;onItemsRendered&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;visibleStopIndex&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="c1"&gt;// near the end of loaded data → fetch next page&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;visibleStopIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;leads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nf"&gt;loadMore&lt;/span&gt;&lt;span class="p"&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;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;style&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&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;leads&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&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;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FixedSizeList&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;The &lt;code&gt;style&lt;/code&gt; prop is non-negotiable — it carries the &lt;code&gt;position: absolute; top&lt;/code&gt; that places each row. Forget to spread it and every row stacks at the top.&lt;/p&gt;




&lt;h2&gt;
  
  
  So... did my 100 rows need virtualization?
&lt;/h2&gt;

&lt;p&gt;No. And this is the part most posts skip.&lt;/p&gt;

&lt;p&gt;100 simple rows ≈ 500–1,000 DOM nodes. Browsers comfortably handle tens of thousands. Initial render: single-digit milliseconds. And scrolling static content is nearly free — the browser paints once and just shifts pixels.&lt;/p&gt;

&lt;p&gt;Where lists actually get slow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Row complexity, not row count.&lt;/strong&gt; 100 rows × images, menus, icons, nested components = 10,000+ nodes and 100 component instances. Now it hurts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change detection, not the DOM.&lt;/strong&gt; If data updates every second (live feeds, WebSockets), every rendered row gets re-checked on every cycle. Fewer rendered rows = cheaper cycles. Though the better first fix is &lt;code&gt;OnPush&lt;/code&gt; + &lt;code&gt;trackBy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation, not scrolling.&lt;/strong&gt; Sorting or filtering 2,000 rendered rows means destroying and recreating thousands of nodes — that's the visible freeze.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My rules now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;≤500 simple rows, static-ish data&lt;/strong&gt; → plain &lt;code&gt;*ngFor&lt;/code&gt; + &lt;code&gt;trackBy&lt;/code&gt;. Done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frequent updates&lt;/strong&gt; → &lt;code&gt;OnPush&lt;/code&gt; + &lt;code&gt;trackBy&lt;/code&gt; first. Usually enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,000+ rows, or unbounded data&lt;/strong&gt; (leads, logs, transactions) → virtualize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public/SEO pages, Ctrl+F-heavy tools, print views&lt;/strong&gt; → don't virtualize. Off-screen content doesn't exist, so browser search finds nothing and crawlers see only the window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Virtualization is a performance fix, not a default. Profile first: DevTools → Performance → record a scroll and a data update. Frames under 16ms? There's no problem to solve.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm &lt;a href="https://linkedin.com/in/anandmohanbabu" rel="noopener noreferrer"&gt;Anand&lt;/a&gt;, a backend-leaning full stack developer working on fintech platforms with Node.js, NestJS, Angular, and SQL. I write about the things I get wrong on the way to getting them right.&lt;/em&gt;&lt;/p&gt;

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