Skip to content

feat(web): recycle offscreen component rows instead of show-more paging#421

Merged
haksungjang merged 1 commit into
mainfrom
feat/components-table-virtualization
Jul 16, 2026
Merged

feat(web): recycle offscreen component rows instead of show-more paging#421
haksungjang merged 1 commit into
mainfrom
feat/components-table-virtualization

Conversation

@haksungjang

Copy link
Copy Markdown
Member

Why

Large SBOMs (server caps the detail rows at 2,000) rendered the components table in 200-row "Show more" batches — nine clicks to reach the tail, and every revealed row stayed in the DOM for the life of the view.

What

  • Rows render in 100-row <tbody> chunks. An IntersectionObserver (600px margin) over the table's scroll container keeps nearby chunks live and recycles offscreen ones into a single spacer row of their measured height — scrollbar geometry stays truthful while the DOM holds only a window.
  • Variable row heights (wrapping license badges, expanded detail panel) are why spacers use per-chunk measurements instead of a fixed row height. The chunk containing the expanded row is pinned live; tbody nodes are keyed by chunk index so observer targets survive live/spacer swaps; measurements reset on any filter/sort/data change.
  • The whole filtered set is now reachable by plain scrolling; the show-more button and its locale keys are removed (en/ko in step).
  • No new dependency.

Verification

  • New components-virtual.spec.ts (600-component stub): no show-more button, DOM row count < total at the top and after scrolling to the bottom (top chunks recycled), last component materializes on scroll, in-chunk row expansion works — 2/2 pass
  • Full Playwright UI suite (incl. axe specs): 56/56 pass
  • tsc --noEmit, vitest 216/216, i18n en ≡ ko (529 keys), token lint — pass

Large SBOMs rendered the components table in 200-row "Show more"
batches: reaching the tail of a 2,000-row result took nine clicks, and
every revealed row stayed in the DOM for the life of the view.

Render the filtered set in 100-row <tbody> chunks instead. An
IntersectionObserver over the scroll container keeps chunks near the
viewport live and recycles the rest into a single spacer row of their
measured height, so the scrollbar geometry stays truthful while the
DOM only ever holds a window. Row heights may vary (wrapping license
badges, the expanded detail panel), which is why spacers use per-chunk
measurements rather than a fixed row height; the chunk holding the
expanded row is pinned live, and the tbody nodes are keyed by chunk
index so the observer targets survive live/spacer swaps. The whole
list is now reachable by plain scrolling — the show-more button and
its locale keys are gone.

A Playwright spec drives a 600-component scan: full set scrollable,
DOM row count stays below the total at both ends of the scroll, and
in-chunk row expansion still works.
@haksungjang
haksungjang merged commit 86a6f61 into main Jul 16, 2026
35 checks passed
@haksungjang
haksungjang deleted the feat/components-table-virtualization branch July 16, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant