The Illusion of Infinite Chat

The Illusion of Infinite Chat


Try something. Open a very long ChatGPT conversation — one with hundreds of messages — and select some text near the top. Now scroll all the way down.

Your selection is gone.

The text is still there. The conversation hasn't changed. But the selection vanished the moment you scrolled away. If you scroll back, the text reappears — but your highlight doesn't.

This isn't a bug. It's the side effect of a design pattern that powers every major chat application on the web.

The Problem with 10,000 Messages

A naive chat UI would render every message as a DOM node. Ten thousand <div> elements, each with text, timestamps, avatars, event listeners, and layout calculations. The browser dutifully computes geometry for all of them — even the 9,950 that are nowhere near the viewport.

Scrolling slows. Memory climbs. On mobile, the tab crashes.

The fix seems obvious: don't render what nobody can see.

Virtualization: The Moving Window

Virtualization means only rendering the messages currently visible on screen, plus a small buffer. Everything else is simply not in the DOM.

The demo below shows the difference. On the left, a naive implementation renders all 200 messages. On the right, the virtualized version only renders what's near the viewport. Watch the DOM node counter as you scroll.

Live Demo

As you scroll the right panel, notice the counter stays around 15–20 nodes while the left panel holds all 200. In a real app with 10,000+ messages, the naive approach would create 10,000 DOM elements. The virtualized approach would still only render ~30.

The Sliding Window Cache

Virtualization solves rendering. But the data itself needs management too.

A 10,000-message conversation is too large to fetch in one API call. So the client uses cursor-based pagination — fetching chunks as you scroll, and evicting chunks that are far away to keep memory bounded.

The demo below visualizes this. Use the slider to "scroll" through the conversation. Watch which pages are in the browser cache and which get evicted.

Live Demo

This is a sliding window cache — the same idea behind OS page caches and database buffer pools. Keep what's hot, discard what's cold, fetch on demand.

What Survives the Window (And What Doesn't)

Here's the interesting part.

When you "star" a message in Slack, or react with 👍 in Discord, the application stores that in its own state — Redux, Zustand, a context store. This state is independent of the DOM. When message 123 scrolls out of view and the virtualizer unmounts it, the bookmark doesn't disappear. When it scrolls back in, the component reads state and renders the star again.

Native browser text selection is different.

When you click-drag to highlight text, the browser stores the selection as references to actual DOM nodes:

// What the browser's Selection API stores internally
{
  anchorNode: <span> inside msg_100,   // a live DOM element
  anchorOffset: 14,
  focusNode: <span> inside msg_100,
  focusOffset: 52
}

These are pointers to live DOM elements. The moment the virtualizer unmounts message 100, those DOM nodes are destroyed. The browser's selection is pointing at ghosts.

Try it yourself. In the demo below, select some text in one of the top messages, then scroll down. Scroll back up. Your selection is gone — even though the text is back.

Live Demo

That's the fundamental asymmetry:

Action Stored in Survives?
Bookmark message Application state Yes
Add reaction Application state Yes
Pin message Application state Yes
Expand code block Application state Yes
Mouse drag selection Browser Selection API No
Cursor position Browser Selection API No

The application can preserve meaning (bookmarks, reactions, pins). It cannot automatically preserve browser-native interactions — those are bound to physical DOM nodes.

Could an app preserve drag selection?

Technically yes. The app could intercept window.getSelection(), convert it to a logical reference ({ messageId, startOffset, endOffset }), and restore it via Selection.addRange() when the element remounts. But this is fragile — code blocks, markdown rendering, images, and collapsed sections all shift offsets. Most apps don't attempt it.

This is the same reason Google Docs and Notion build their own document model and selection system instead of relying on the browser's.

The Full Architecture

Zoom out and the pipeline looks like this:

  Conversation Database
          │
   Cursor Pagination          "Give me messages before X"
          │
    Client Page Cache         ← sliding window of data
          │
     Virtualizer              ← sliding window of DOM
          │
    React Components          ← what you see
          │
  ┌───────┴────────┐
  │  Application   │         ← bookmarks, reactions, pins
  │  State Store   │            (survives DOM destruction)
  └────────────────┘

Two sliding windows stacked on top of each other. One controls which data is in memory. The other controls which data is in the DOM. And a separate state layer sits alongside — holding everything the user has done, independent of both.

The DOM is the most disposable layer in the stack.

This Powers More Than Chat

The same architecture appears in:

  • Slack / Discord / WhatsApp Web — message virtualization with infinite scroll
  • Twitter/X / Reddit — feed virtualization with cursor pagination
  • VS Code — file content virtualized for large files
  • Google Sheets — only visible cells are rendered
  • Chrome DevTools — network/console logs use windowed lists

Any application displaying a potentially unbounded list of items eventually converges on this pattern: paginate the data, virtualize the rendering, separate the state.

The Takeaway

A modern chat interface is not a page full of messages. It's a narrow viewport sliding over a data stream, rendering only the slice you're looking at, and maintaining a separate layer of state for everything you've done.

The next time your text selection vanishes mid-scroll in ChatGPT, you'll know why. The DOM node you selected doesn't exist anymore. It was never meant to be permanent.

In a virtualized world, the interface is temporary. The data and your intent are what persist.