<?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: Shree Harsha Boyapati</title>
    <description>The latest articles on DEV Community by Shree Harsha Boyapati (@shreeharsha_boyapati).</description>
    <link>https://dev.to/shreeharsha_boyapati</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%2F4063581%2F622e6aa6-55a0-4fd9-85f0-efdeca874dd7.png</url>
      <title>DEV Community: Shree Harsha Boyapati</title>
      <link>https://dev.to/shreeharsha_boyapati</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shreeharsha_boyapati"/>
    <language>en</language>
    <item>
      <title>Rendering Large Lists on the UI — A Pragmatic Guide</title>
      <dc:creator>Shree Harsha Boyapati</dc:creator>
      <pubDate>Wed, 05 Aug 2026 06:54:25 +0000</pubDate>
      <link>https://dev.to/shreeharsha_boyapati/rendering-large-lists-on-the-ui-a-pragmatic-guide-49na</link>
      <guid>https://dev.to/shreeharsha_boyapati/rendering-large-lists-on-the-ui-a-pragmatic-guide-49na</guid>
      <description>&lt;p&gt;I was working on a couple of projects — one displaying FTP folders and files, another powering a RAG system — where I needed to show lists of data in cards. The lists had to handle large datasets without lagging, crashing, or freezing the browser. I came up with a four-phase plan, tested it in those projects, and then rebuilt it here as a clean reference implementation. This post walks through each phase, what problem it solves, and what trade-offs remain.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering" rel="noopener noreferrer"&gt;ShreeHarshaBoyapati/List-Rendering&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 1: Initial List Rendering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commit:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/75c2be7076f5c1c5d70e12e446cbdbfabd9fb009" rel="noopener noreferrer"&gt;&lt;code&gt;75c2be7&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The simplest approach: fetch every item from the backend in one go and render every card as a DOM node.&lt;/p&gt;

&lt;p&gt;The backend was an Express server with an in-memory array — no pagination, no limit. The frontend called &lt;code&gt;GET /api/items&lt;/code&gt;, received the entire dataset as JSON, and rendered it with &lt;code&gt;items.map(...)&lt;/code&gt; inside a &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;. CRUD operations were straightforward: &lt;strong&gt;Create&lt;/strong&gt; sent a &lt;code&gt;POST&lt;/code&gt; and prepended the new item; &lt;strong&gt;Update&lt;/strong&gt; sent a &lt;code&gt;PATCH&lt;/code&gt; and spliced the item in place; &lt;strong&gt;Delete&lt;/strong&gt; sent a &lt;code&gt;DELETE&lt;/code&gt; and filtered the item out. After each mutation, the app called &lt;code&gt;loadInitial()&lt;/code&gt; — a full refetch and re-render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; All the JS objects (the entire dataset) sat in the heap, and every card's DOM node was also created and stored in the heap. For 100 items this was fine. For 50,000 it was not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Slow rendering after fetch.&lt;/strong&gt; Even after the backend responded, the UI froze for seconds while the browser created 50,000 DOM nodes. The fetch was done, but the user saw nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON body limit.&lt;/strong&gt; When the dataset grew large enough, the response exceeded the JSON body size limit configured in the backend middleware. The request failed outright — no data was rendered at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heap overflow.&lt;/strong&gt; Even if the fetch succeeded, the browser's heap could fill up. The GC collector would thrash trying to free memory, and in extreme cases the tab would crash.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demos:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630232719-32daa580-f1c9-4309-a972-dcda8d95a9fe.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065426Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=1ee23b7b5592725931f2cd3fb57fbd0a42fceab53d382786e7687ff38475d577&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233016-afa3508c-9347-42d3-b7ba-0445ed440951.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065427Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=c7a7c512d1d2ecd7c114608a50e93c44cc6ee0175a6e7d0ab45e60260b22b2b4&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  Phase 2: Virtualization
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commit:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/447c57d5d70e8766bdfa36fe2163e9657e78baf6" rel="noopener noreferrer"&gt;&lt;code&gt;447c57d&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first problem — slow rendering — was caused by creating DOM nodes for every item. Virtualization fixes this by only rendering the cards visible in the viewport (plus a small overscan buffer).&lt;/p&gt;

&lt;p&gt;I added &lt;a href="https://github.com/bvaughn/react-window" rel="noopener noreferrer"&gt;&lt;code&gt;react-window&lt;/code&gt;&lt;/a&gt;, which provides a &lt;code&gt;&amp;lt;List&amp;gt;&lt;/code&gt; component. You give it a &lt;code&gt;rowHeight&lt;/code&gt; (80px), a &lt;code&gt;rowCount&lt;/code&gt; (the total number of items), and a custom &lt;code&gt;Row&lt;/code&gt; renderer. The library handles the rest — it mounts only the ~10–15 rows that fit on screen and recycles them as you scroll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; The JS objects for all items still lived in the heap, but now only a handful of DOM nodes existed at a time. The virtualization library also maintained a height map in the heap, but this was negligible compared to the full DOM tree from Phase 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The win:&lt;/strong&gt; Rendering 50,000 items became instant. The browser only ever had a dozen DOM nodes to worry about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What remained:&lt;/strong&gt; The backend still sent all 50,000 items in a single JSON response. If the dataset was large enough, the JSON body limit would still kill the request, and the heap could still overflow from the JS objects alone — no DOM needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233107-7dc1d423-154f-4e87-88b9-7eaa7e8d3394.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065430Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=7e94ce56e128d29486d907c586bffd2b5b38ae4caad873a7fb10a723819a7016&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A note on library choice: before reaching for &lt;code&gt;react-window&lt;/code&gt; or any virtualization library, check its bundle size, maintenance status, and vulnerabilities. You can also build your own — it's not much code for fixed-height rows — but I'd start with a proven library.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Cursor Pagination + Sliding Window
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commits:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/8951667bf6cdfbfc705f092e15fe81027817beb5" rel="noopener noreferrer"&gt;&lt;code&gt;8951667&lt;/code&gt;&lt;/a&gt; (pagination) · &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/14441fce84d813f4b7772eb17b6f1f749b6eda72" rel="noopener noreferrer"&gt;&lt;code&gt;14441fc&lt;/code&gt;&lt;/a&gt; (CRUD fixes)&lt;/p&gt;

&lt;p&gt;Phase 2 solved the DOM problem. Phase 3 solves the data problem: instead of fetching everything at once, fetch it in pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor vs. Offset Pagination
&lt;/h3&gt;

&lt;p&gt;I used &lt;strong&gt;cursor (keyset) pagination&lt;/strong&gt;, not offset pagination. The cursor is a &lt;code&gt;base64url&lt;/code&gt;-encoded string of &lt;code&gt;"createdAt,id"&lt;/code&gt; — a compound keyset sorted by &lt;code&gt;createdAt DESC, id ASC&lt;/code&gt;. It's a boundary value: "give me everything that sorts &lt;em&gt;after&lt;/em&gt; this point."&lt;/p&gt;

&lt;p&gt;The key difference: &lt;strong&gt;offsets shift when rows are inserted or deleted; keyset cursors don't.&lt;/strong&gt; If you're on page 3 at offset 100 and someone deletes a row above you, offset 100 now points to a different row. A cursor pointing to a specific &lt;code&gt;(createdAt, id)&lt;/code&gt; boundary stays valid no matter what happens elsewhere in the table.&lt;/p&gt;

&lt;p&gt;On the backend (now backed by Postgres via TypeORM), &lt;code&gt;findNextPage&lt;/code&gt; queries &lt;code&gt;WHERE createdAt &amp;lt; cursor.createdAt OR (createdAt = cursor.createdAt AND id &amp;lt; cursor.id)&lt;/code&gt;, ordered &lt;code&gt;DESC&lt;/code&gt;. &lt;code&gt;findPrevPage&lt;/code&gt; inverts the condition and ordering, then reverses the result to restore &lt;code&gt;DESC&lt;/code&gt; order.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Sliding Window
&lt;/h3&gt;

&lt;p&gt;The frontend keeps at most &lt;code&gt;MAX_PAGES = 3&lt;/code&gt; pages (150 items) in memory. When a new page is fetched at the bottom, the top page is evicted. When a page is fetched at the top, the bottom page is evicted. A &lt;code&gt;pageBoundariesRef&lt;/code&gt; tracks which slice of the &lt;code&gt;items[]&lt;/code&gt; array belongs to which page, along with each page's cursors.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Scroll Problem
&lt;/h3&gt;

&lt;p&gt;Here's the tricky part: when you prepend items (scrolling up), the browser's &lt;code&gt;scrollTop&lt;/code&gt; doesn't change, but the content above has grown — so the viewport jumps to a different item. The user loses their place.&lt;/p&gt;

&lt;p&gt;The fix: capture &lt;code&gt;scrollTop&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; the prepend. Then, in a &lt;code&gt;useEffect&lt;/code&gt; that runs after React has committed the new items, restore the scroll position using pure arithmetic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;newScrollTop = oldScrollTop + prependedCount × ITEM_HEIGHT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No DOM queries needed — &lt;code&gt;react-window&lt;/code&gt; hasn't rendered the anchored row yet at the new offset, so &lt;code&gt;querySelector&lt;/code&gt; would fail. The formula is exact because both states (before capture and after restore) have no loading row, so the math is clean.&lt;/p&gt;
&lt;h3&gt;
  
  
  CRUD Without Full Reload
&lt;/h3&gt;

&lt;p&gt;Commit &lt;code&gt;14441fc&lt;/code&gt; fixed the CRUD operations to work with the sliding window instead of calling &lt;code&gt;loadInitial()&lt;/code&gt; after every mutation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Op&lt;/th&gt;
&lt;th&gt;Handling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Create&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If at the true top (&lt;code&gt;!hasPrev&lt;/code&gt;): prepend to &lt;code&gt;items[]&lt;/code&gt;, shift page boundaries. Else: just set &lt;code&gt;hasPrev = true&lt;/code&gt; — the new item exists above the window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Splice the updated item in place by &lt;code&gt;id&lt;/code&gt;. Sort position is unchanged (&lt;code&gt;createdAt&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; are immutable), so all cursors stay valid.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delete&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Filter the item out locally. If the window becomes empty and &lt;code&gt;hasNext&lt;/code&gt;, auto-trigger &lt;code&gt;loadNext()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No full reloads, no scroll jumps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A note on TanStack Query: I've seen teams adopt it to reduce CRUD boilerplate. It's genuinely useful when you need to &lt;strong&gt;invalidate a cache from a different component&lt;/strong&gt;. But if your CRUD lives in a single component, plain &lt;code&gt;fetch&lt;/code&gt; (or &lt;code&gt;axios&lt;/code&gt;) is leaner — smaller bundle, fewer abstractions, and the code is just as readable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; Similar to Phase 2, but the dataset coming from the backend is tiny — only one page at a time. The heap holds at most 150 items. It's essentially a sliding window.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233195-404ce920-5e8b-4ce4-8cb2-c9eb3261dfc7.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065431Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=33d378c5d50aa7d19cfece95067c903138a7daf669bf35815505f6c7b878e56a&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What remained:&lt;/strong&gt; An edge case where a single page contains one extremely large item. The JSON response for that page could still exceed the body limit, or the heap could spike. I hardcoded the page limit, but you could make it dynamic — monitor performance metrics and adjust the limit up or down in a &lt;code&gt;useEffect&lt;/code&gt; to keep things smooth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4: IndexedDB Cache Layer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commit:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/3b804ecafde562a65da6f5135009bb5980c1c057" rel="noopener noreferrer"&gt;&lt;code&gt;3b804ec&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Phase 3 fixed the data problem but introduced a new one: every time a page was evicted and the user scrolled back, the app hit the backend again. Phase 4 adds a client-side cache to avoid those redundant fetches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-Tier Storage
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Heap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React state &lt;code&gt;items[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Currently visible window&lt;/td&gt;
&lt;td&gt;3 pages (150 items)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. IndexedDB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser DB &lt;code&gt;rendering-list-cache&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Recently evicted pages — 3 above + 3 below&lt;/td&gt;
&lt;td&gt;6 pages (300 items)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;All items&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a page is evicted from the heap, it's written to IndexedDB. When the user scrolls back, the app checks IDB first. A &lt;strong&gt;cache hit&lt;/strong&gt; means no backend call at all. A &lt;strong&gt;cache miss&lt;/strong&gt; falls through to the backend, and the result gets cached on future eviction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache Key Scheme
&lt;/h3&gt;

&lt;p&gt;Pages are keyed by the cursor that would be used to fetch them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top-evicted page&lt;/strong&gt; (evicted during &lt;code&gt;loadNext&lt;/code&gt;): stored as &lt;code&gt;prev:${cursor}&lt;/code&gt; — scrolling back up calls &lt;code&gt;loadPrev(cursor)&lt;/code&gt;, so we look up &lt;code&gt;prev:${cursor}&lt;/code&gt; in IDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom-evicted page&lt;/strong&gt; (evicted during &lt;code&gt;loadPrev&lt;/code&gt;): stored as &lt;code&gt;next:${cursor}&lt;/code&gt; — scrolling back down calls &lt;code&gt;loadNext(cursor)&lt;/code&gt;, so we look up &lt;code&gt;next:${cursor}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a page is restored from IDB to the heap, it's deleted from IDB — a page exists in &lt;strong&gt;exactly one tier at a time&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  CRUD with IDB Awareness
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create:&lt;/strong&gt; If the topmost cached page in IDB is the true top (&lt;code&gt;hasPrev = false&lt;/code&gt;), the new item is prepended to that cached page so the user sees it on scroll-up without a backend call. Otherwise, no sync — the backend serves it on cache miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update / Delete:&lt;/strong&gt; Only touch the heap. Since a page lives in exactly one tier, if the item is in the heap, it's not in IDB — no IDB sync needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233242-f9d8c8e1-e055-4900-917d-3e766b6440bc.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065434Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=5a9c679a67e7135dec5ccb5db83bc34b301a95b78302450f5a8c0895b300abc9&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What remained:&lt;/strong&gt; The same edge case as Phase 3 — a single extremely large item in a page could still exceed limits. IDB doesn't solve that; it solves the &lt;em&gt;number of backend calls&lt;/em&gt;, not the &lt;em&gt;size of a single response&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What it solves&lt;/th&gt;
&lt;th&gt;What remains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Initial render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Baseline — fetch all, render all&lt;/td&gt;
&lt;td&gt;Slow render, JSON limit, heap overflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Virtualization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DOM node count — only visible rows rendered&lt;/td&gt;
&lt;td&gt;JSON limit, heap overflow (all data still fetched)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Cursor pagination&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data volume — only one page fetched at a time; CRUD-safe cursors&lt;/td&gt;
&lt;td&gt;Single huge item per page could still hit limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. IndexedDB cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Backend call count — evicted pages cached client-side&lt;/td&gt;
&lt;td&gt;Same single-huge-item edge case&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each phase addresses a specific bottleneck without regressing on the previous one. The result is a list that can handle tens of thousands of items smoothly — visible rows are virtualized, data is paginated with CRUD-safe cursors, the heap is bounded by a sliding window, and recently viewed pages are cached in IndexedDB to avoid redundant fetches.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
  </channel>
</rss>
