UI Virtualization Catalog
How UI frameworks render a viewport onto a data set far larger than it — list virtualization, container recycling, builder delegates, clippers and the model protocols underneath them.
This survey exists to inform one decision: how hue's DSV data browser should scroll a multi-thousand-row grid built on sparkles:ui, a canvas-first immediate-mode toolkit. It complements three neighbouring catalogs — UI Layout (how widgets get sizes), UI backend seam (how a display list reaches a device) and TUI Libraries (terminal rendering models) — none of which covers the viewport-versus-data-size axis.
Last reviewed: August 30, 2026
The questions this survey answers
- Is clipping enough, and if not, why not? → Dear ImGui, which answers it in its own header.
- What do immediate-mode toolkits do, given that they rebuild everything every frame? → Dear ImGui, egui, Gio
- What does recycling buy, and what does it cost? → Qt Quick, GTK 4, WPF / Avalonia, VS Code
- How does a scrollbar describe content that was never measured? → concepts § virtual extent, Gio, egui
- What breaks when a view no longer contains every row? → concepts § what breaks — identity, focus, accessibility, hit testing
- What does a terminal change about the problem? → Textual, Ratatui
- Where does sparkles stand, measured rather than guessed? → sparkles baseline
- So what should sparkles do? → comparison
Master catalog
| Subject | Language | Mode | Mechanism | Model protocol | Recycles | Link |
|---|---|---|---|---|---|---|
| Dear ImGui | C++ | immediate | ImGuiListClipper index range | ✗ (caller's loop) | ✗ | → |
| egui | Rust | immediate | ScrollArea::show_rows(Range) | ✗ (closure) | ✗ | → |
| Gio | Go | immediate | layout.List + ListElement builder | ✗ (closure) | ✗ | → |
| Ratatui | Rust | immediate (TUI) | ListState::offset + bounded walk | ✗ (owns a Vec) | ✗ | → |
| Textual | Python | retained (TUI) | render_line(y) / render_lines(crop) | subclass-defined | ✗ | → |
| Flutter | Dart | retained | slivers + SliverChildBuilderDelegate | builder | elements | → |
| GTK 4 | C | retained | GListModel + GtkListItemFactory | GListModel | widgets | → |
| Qt Quick | C++/QML | retained | ListView + reuseItems pool | QAbstractItemModel | delegates | → |
| WPF / Avalonia | C# | retained | VirtualizingStackPanel | ItemsSource | containers | → |
| VS Code | TypeScript | retained | range diff + RowCache + RangeMap | IListVirtualDelegate | DOM rows | → |
| Slint | Rust | retained | Repeater::ensure_updated_listview | Model | instances | → |
| sparkles (hue DSV) | D | immediate | DsvWindow (DSN4) + DsvModel (DSN7) | retained model | ✗ | → |
Taxonomy
By what the window bounds
| Bounds | Subjects |
|---|---|
| Paint and measure only | Ratatui |
| Build, layout, paint | WPF / Avalonia (default), VS Code |
| …and data access | Dear ImGui, egui, Gio, Textual, Flutter, GTK 4, Qt Quick, Slint, sparkles |
By how the visible range is computed
| Strategy | Random access | Variable sizes | Subjects |
|---|---|---|---|
| Division by a uniform size | O(1) | ✗ | Dear ImGui, egui, Slint, sparkles |
| Walk from an index anchor | relative only | ✓ | Gio, Ratatui, Flutter, GTK 4, Qt Quick, WPF / Avalonia |
| Prefix sums over declared sizes | O(log N) | ✓ | VS Code |
| Compositor-resolved lines | O(1) | n/a | Textual |
By how the scroll extent is known
| Strategy | Subjects |
|---|---|
| Declared (exact, no measurement) | egui, Textual, Slint, VS Code, Dear ImGui, sparkles |
| Estimated from measured items | Gio, GTK 4, Qt Quick, WPF / Avalonia |
| Progressive (exact where visited) | Flutter |
By setup/bind split
| Form | Subjects |
|---|---|
| Explicit two-phase API | GTK 4 (setup/bind), VS Code (renderTemplate/renderElement), Slint (init/update) |
| Split across object kinds | Flutter (widget vs element) |
| Implicit, via a pool lifecycle | Qt Quick (pooled/reused), WPF / Avalonia |
| None — rebuild, preserve identity separately | Dear ImGui, egui, Gio, Ratatui, sparkles |
Milestones
| Year | Event |
|---|---|
| 1996 | Windows LVS_OWNERDATA — the Win32 "virtual list view", a callback per visible item |
| 2006 | WPF ships VirtualizingStackPanel with VirtualizationMode.Standard |
| 2008 | WPF 3.5 SP1 adds VirtualizationMode.Recycling and VirtualizationCacheLength |
| 2009 | Qt Quick 1.0 ListView instantiates only visible delegates |
| 2014 | Android RecyclerView makes "recycling" the mainstream name for the idea |
| 2015 | Dear ImGui gains ImGuiListClipper (evolving out of the earlier CalcListClipping) |
| 2017 | Flutter's sliver protocol and ListView.builder |
| 2020 | Qt 5.15 adds ListView.reuseItems — recycling, opt-in |
| 2020 | GTK 4 ships GtkListView / GtkListItemFactory, retiring GtkTreeView |
| 2026 | sparkles DSN4 — hue's DSV grid renders a row window, then DSN7 retains the model it is a window onto (baseline) |
NOTE
Dates before 2020 are from release history rather than from the pinned trees this survey reads; treat them as orientation, not as citations.
Reading paths
"I want the short version." → comparison § the consensus, then comparison § what this means for sparkles.
"I'm designing virtualization for an immediate-mode toolkit." → concepts → Dear ImGui → egui → Gio → sparkles baseline → comparison.
"I want to know whether to recycle." → Qt Quick (the hazards, stated by the vendor) → GTK 4 (the setup/bind split) → VS Code (the minimal range diff) → comparison § architectural trade-offs.
"My scrollbar is wrong." → concepts § virtual extent → Gio (estimation, honestly) → VS Code (prefix sums, exactly).
"I'm doing this in a terminal." → Textual → Ratatui → sparkles baseline.
Sources
Every subject page cites the upstream tree it was read from, pinned to a commit. The sparkles figures come from a phase-decomposition benchmark in apps/hue/src/dsv_bench.d; see the baseline.