Skip to content

Table virtualization for sub-second response on 10k+ keyed ranges #109

Description

@adnaan

Context

The streaming-range work and per-op DOM mutation in #107 (PR #108) drove single-row delete on a 10k-row table from 6-8 s to ~1.17 s browser-side (4-5× improvement). Manual iPhone testing confirms the freeze is gone but the lag is still "quite noticeable" — likely 2-3 s on iPhone Safari given mobile's slower layout cost.

Profile of the residual ~1.17 s:

Phase Cost
Server compute + WS RTT ~150 ms
Client updateDOM JS ~447 ms
Browser layout/paint of 10k-row table after one row removed ~570 ms

The browser layout cost is the floor. Even driving JS to 0 ms would still leave ~720 ms — and that's headless Chrome on a fast Linux box. iPhone Safari is 2-3× slower at table layout.

Why this can't be fixed at the JS layer

  • The PR-feat: per-op targeted DOM mutation for range diff ops (#107) #108 path already mutates the DOM in O(1) for keyed range ops.
  • Morphdom is short-circuited via data-lvt-targeted-skip for the affected range subtree.
  • Post-render directive scans are skipped when no nodes were added.
  • Server-side per-render is already ~75 ms (Phase 7).

What remains is the cost of having 10k DOM elements on the page. Every layout operation walks them; every paint touches them. The only way to cut this cost is to not have them in the DOM.

Proposed approach: virtualized range

Add a lvt-virtual attribute (or similar) on the range container that opts into a windowed render:

```html

{{range .Items}} ... {{end}}
\`\`\`

Client behavior:

  1. On first render, only insert the visible window (e.g. 30 rows for a 600 px viewport) + a small buffer (5 above, 5 below).
  2. Maintain spacer rows (or padding-top/padding-bottom) so the scrollbar reflects the full data extent.
  3. On scroll, swap in/out rows by data-key as the window slides.
  4. Per-op range diffs (r/u/i/a/p/o) apply to the virtual data structure; only the visible subset hits the DOM.

DOM stays at ~50 elements regardless of dataset size. Layout cost becomes O(visible) not O(total).

Alternatives considered

  • content-visibility: auto (CSS-only): one-line drop-in that tells the browser to skip layout/paint for off-screen rows. Likely brings cost down 5-10×. iOS 18+ only; no Firefox support. Could ship as an opt-in CSS class for users on supported browsers, but doesn't help everyone. Worth trying as a quick experiment regardless.
  • Pagination at the data level: server limits result set to ~100 rows, user navigates pages. Solves the problem but changes the demo's premise ("10,000 rows interactive" is the whole point).
  • Server-side virtualization: server tracks viewport offset, only sends visible window over WS. Most efficient but requires WS protocol changes and viewport state on client.

Scope

Verification

  • New chromedp E2E for LargeTable_DeleteLatency_10k should drop to <300 ms wall-clock.
  • iPhone manual test should feel instant.
  • All existing range op tests must continue to pass with lvt-virtual enabled and disabled.

Filed from

PR #108 (per-op DOM mutation) + iPhone manual testing on 2026-05-02.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions