Long lists like Discord: draw only what is near, cache some, refetch the rest #236

Closed
opened 2026-10-21 19:17:27 +00:00 by littlefox · 2 comments
Owner

The user, 2026-10-21: "I am especially interested in what happens when scrolling back and forth between posts and replies. Will the page grow outside all bounds or will only the posts that you are "near" be actually on the page? I am thinking of how discord handles long chats. You can scroll and scroll, but discord will only hold so many messages in the drawable window. Some are cached in the background and others still are marked as stale to be re-fetched from the server"

What NeoKOM does today

There is no windowing anywhere: everything loaded is on the page.

Thread view (ThreadPane):

  • Up to THREAD_CAP = 100 cards, plus 100 more for each "Visa fler". Never fewer.
  • Every card is in the DOM. Unread cards render their full body, pictures included; read cards render a snippet.
  • Moving to another post in the same thread keeps it all. Moving to another thread drops the cards (the pane is keyed on the root and previews is cleared), so it is bounded per thread, apart from the bug in #235.

Channel view (ChannelPostsPane, #176):

  • 20 posts per "Visa äldre" click, prepended to the list and never dropped.
  • Every post renders its full body, pictures included. 50 clicks give 1 000 full posts on the page.
  • Opening a post unmounts the list. "← Tillbaka till inläggen" mounts it again from scratch: it fetches page 1 again, loses the position and loses the older pages. So the list is too big while you are in it, and gone when you come back.

Behind the page (kom-client):

  • text_stats, bodies and uconfs keep everything fetched, for the whole session, with no limit.
  • Nothing is ever marked stale, except that a new comment drops its parent's stat. Reactions, read confirmations and other aux changes made by others are not seen until the next session.

What it should be (Discord-like, to settle in DESIGN.md)

  1. Drawn: only the posts near the viewport, a few screens' worth, in the DOM. The rest become spacers of their measured (or estimated) height, so the scrollbar and position stay right. This applies to the thread view, the channel view and long inbox chats.
  2. Kept: a bounded cache of what was near recently: the decoded views, an LRU by count and bytes, with pictures counted at their size. Scrolling back within it is instant.
  3. Stale: what fell out of the cache keeps only its number and its height. Scrolling back to it fetches it again. Cached entries older than some minutes (or flagged by an async message) are refetched when they come into view, so reactions and edits show up.
  4. Load on scroll: the channel view loads older pages as you near the top. No button, but the button stays for keyboard and screen-reader users.
  5. Coming back: returning from a post to a channel's list, or to a thread, restores the list and its position from the cache, instead of starting over.

Things to keep working

  • Scroll position must not jump when posts above are added or dropped (anchor on the post in view, see #229).
  • Read-by-seen (#180) must only consider drawn posts.
  • Search in page (Ctrl+F) will only find drawn posts; Discord accepts this, we will too, and the local search (#154) covers the rest.
  • Screen readers: keep aria-setsize/aria-posinset on the list items.
  • e2e specs that count .thread-item need to change.

Measure with #225: DOM nodes and memory after scrolling 2 000 posts back and forth, which should stay flat, and the time to scroll back to the start.

Related: #226 (redraws), #227 (memory), #229 (settling).

The user, 2026-10-21: "I am especially interested in what happens when scrolling back and forth between posts and replies. Will the page grow outside all bounds or will only the posts that you are "near" be actually on the page? I am thinking of how discord handles long chats. You can scroll and scroll, but discord will only hold so many messages in the drawable window. Some are cached in the background and others still are marked as stale to be re-fetched from the server" ## What NeoKOM does today There is no windowing anywhere: everything loaded is on the page. **Thread view** (`ThreadPane`): - Up to `THREAD_CAP = 100` cards, plus 100 more for each "Visa fler". Never fewer. - Every card is in the DOM. Unread cards render their **full body**, pictures included; read cards render a snippet. - Moving to another post in the same thread keeps it all. Moving to another thread drops the cards (the pane is keyed on the root and `previews` is cleared), so it is bounded per thread, apart from the bug in #235. **Channel view** (`ChannelPostsPane`, #176): - 20 posts per "Visa äldre" click, prepended to the list and never dropped. - Every post renders its **full body**, pictures included. 50 clicks give 1 000 full posts on the page. - Opening a post unmounts the list. "← Tillbaka till inläggen" mounts it again from scratch: it fetches page 1 again, loses the position and loses the older pages. So the list is too big while you are in it, and gone when you come back. **Behind the page** (`kom-client`): - `text_stats`, `bodies` and `uconfs` keep everything fetched, for the whole session, with no limit. - Nothing is ever marked stale, except that a new comment drops its parent's stat. Reactions, read confirmations and other aux changes made by others are not seen until the next session. ## What it should be (Discord-like, to settle in DESIGN.md) 1. **Drawn:** only the posts near the viewport, a few screens' worth, in the DOM. The rest become spacers of their measured (or estimated) height, so the scrollbar and position stay right. This applies to the thread view, the channel view and long inbox chats. 2. **Kept:** a bounded cache of what was near recently: the decoded views, an LRU by count and bytes, with pictures counted at their size. Scrolling back within it is instant. 3. **Stale:** what fell out of the cache keeps only its number and its height. Scrolling back to it fetches it again. Cached entries older than some minutes (or flagged by an async message) are refetched when they come into view, so reactions and edits show up. 4. **Load on scroll:** the channel view loads older pages as you near the top. No button, but the button stays for keyboard and screen-reader users. 5. **Coming back:** returning from a post to a channel's list, or to a thread, restores the list and its position from the cache, instead of starting over. ## Things to keep working - Scroll position must not jump when posts above are added or dropped (anchor on the post in view, see #229). - Read-by-seen (#180) must only consider drawn posts. - Search in page (Ctrl+F) will only find drawn posts; Discord accepts this, we will too, and the local search (#154) covers the rest. - Screen readers: keep `aria-setsize`/`aria-posinset` on the list items. - e2e specs that count `.thread-item` need to change. Measure with #225: DOM nodes and memory after scrolling 2 000 posts back and forth, which should stay flat, and the time to scroll back to the start. Related: #226 (redraws), #227 (memory), #229 (settling).
Author
Owner

Narrowed 2026-10-21 (the user agreed): #242 replaces the channel and thread views in 1.0, so this issue builds the shared pieces, not a windowed version of today's views:

  • a reusable windowed list component: variable-height items, spacers, anchoring, load-more at both ends, aria-setsize/aria-posinset;
  • a bounded cache in kom-client (count and bytes, LRU) for stats, bodies and built views, with staleness and refetch on view.

Use them in the current views only where it is cheap: the channel view's "Visa äldre" list and coming back to it. The thread view keeps its cap until #242. Designed with #242's layout in mind (mockups there).

Narrowed 2026-10-21 (the user agreed): #242 replaces the channel and thread views in 1.0, so this issue builds the **shared pieces**, not a windowed version of today's views: - a reusable windowed list component: variable-height items, spacers, anchoring, load-more at both ends, `aria-setsize`/`aria-posinset`; - a bounded cache in `kom-client` (count and bytes, LRU) for stats, bodies and built views, with staleness and refetch on view. Use them in the current views only where it is cheap: the channel view's "Visa äldre" list and coming back to it. The thread view keeps its cap until #242. Designed with #242's layout in mind (mockups there).
Author
Owner

Merged in #270 (main #452 green). The channel view draws only the posts near the screen (IntersectionObserver, 1500 px margin); posts further away become empty blocks of the same height. Kom's caches now have budgets (kom_client::lru: 20 000 stats, 32 MB of bodies, 5 000 conferences). Measured with tools/perf.sh, scrolled back through 840 posts: 45 posts drawn, wasm memory 14 MB (was 89 MB), DOM nodes at the end 7 074 (was 17 187). New spec e2e/tests/channel-window.spec.ts; full suite green; screenshots checked.

Merged in #270 (main #452 green). The channel view draws only the posts near the screen (IntersectionObserver, 1500 px margin); posts further away become empty blocks of the same height. Kom's caches now have budgets (`kom_client::lru`: 20 000 stats, 32 MB of bodies, 5 000 conferences). Measured with tools/perf.sh, scrolled back through 840 posts: 45 posts drawn, wasm memory 14 MB (was 89 MB), DOM nodes at the end 7 074 (was 17 187). New spec e2e/tests/channel-window.spec.ts; full suite green; screenshots checked.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
littlefox/neokom#236
No description provided.