Tree at scale (virtual + lazy)
SvTree scaling to 10,100 nodes via fixed-row virtualization (only ~15 rows in the DOM), plus a lazy-loading tree that fetches each folder’s children on first expand with a spinner. Keyboard, selection and cascading checkboxes intact.
A live, editable Svelte 5 component example from the SvGrid gallery (Layout). Read the SvGrid UI component docs for the full API.
What this example shows
SvTree at scale: a virtualized 10,000-node tree (only visible rows in the DOM) and a lazy-loading tree that fetches each folder's children on first expand. Both keep keyboard nav, selection and cascading checkboxes.
Imports, features and API used
Imports: @svgrid/grid
Source code (324-tree-scale.svelte)
<script lang="ts">
/**
* SvTree at scale: a virtualized 10,000-node tree (only visible rows in the
* DOM) and a lazy-loading tree that fetches each folder's children on first
* expand. Both keep keyboard nav, selection and cascading checkboxes.
*/
import { SvTree } from '@svgrid/grid'
import type { SvTreeNode } from '@svgrid/grid'
// ---- 10,000 nodes: 100 folders x 100 files -------------------------------
const bigNodes: SvTreeNode[] = Array.from({ length: 100 }, (_, f) => ({
id: `f${f}`,
label: `Folder ${f + 1}`,
children: Array.from({ length: 100 }, (_, i) => ({ id: `f${f}-${i}`, label: `File ${f + 1}.${i + 1}` })),
}))
const total = 100 + 100 * 100
let selected = $state<string | null>('f0-0')
let expanded = $state<string[]>(['f0'])
// ---- Lazy loading: folders fetch children on first expand ----------------
const lazyNodes: SvTreeNode[] = [
{ id: 'docs', label: 'Documents', lazy: true },
{ id: 'media', label: 'Media', lazy: true },
{ id: 'projects', label: 'Projects', lazy: true },
]
let lazyExpanded = $state<string[]>([])
let lazySelected = $state<string | null>(null)
// Simulate a server round-trip (deterministic - no Math.random).
function loadChildren(node: SvTreeNode): Promise<SvTreeNode[]> {
return new Promise((resolve) =>
setTimeout(() => {
resolve(
Array.from({ length: 5 }, (_, i) => ({
id: `${node.id}-${i}`,
label: `${node.label} item ${i + 1}`,
// Nest one more lazy level to show it recurses.
lazy: i === 0,
})),
)
}, 650),
)
}
</script>
<div class="wrap">
<header>
<h2>Tree at scale</h2>
<p>Two production patterns: a <strong>virtualized {total.toLocaleString()}-node</strong> tree, and a <strong>lazy-loading</strong> tree that fetches children on demand.</p>
</header>
<div class="cols">
<section>
<h3>Virtualized ({total.toLocaleString()} nodes)</h3>
<SvTree
nodes={bigNodes}
{selected}
expandedIds={expanded}
virtual
height={360}
rowHeight={30}
onSelect={(id) => (selected = id)}
onToggle={(id, open) => (expanded = open ? [...expanded, id] : expanded.filter((e) => e !== id))}
/>
<p class="note">Expand a few folders and scroll - only ~15 rows are ever in the DOM.</p>
</section>
<section>
<h3>Lazy-loaded (fetch on expand)</h3>
<SvTree
nodes={lazyNodes}
selected={lazySelected}
expandedIds={lazyExpanded}
{loadChildren}
height={360}
onSelect={(id) => (lazySelected = id)}
onToggle={(id, open) => (lazyExpanded = open ? [...lazyExpanded, id] : lazyExpanded.filter((e) => e !== id))}
/>
<p class="note">Each folder spins while it loads (~650ms), then reveals its children. The first child is itself lazy.</p>
</section>
</div>
</div>
<style>
.wrap { padding: 20px; max-width: 720px; display: flex; flex-direction: column; gap: 18px; }
header h2 { margin: 0 0 4px; font-size: 20px; font-weight: 700; }
header p { margin: 0; color: var(--sg-muted, #64748b); font-size: 13.5px; line-height: 1.5; }
.cols { display: flex; gap: 28px; flex-wrap: wrap; align-items: start; }
section h3 { margin: 0 0 10px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--sg-muted, #64748b); }
.note { margin: 8px 0 0; font-size: 12px; color: var(--sg-muted, #94a3b8); max-width: 300px; line-height: 1.5; }
</style>Related documentation
Related articles
- Building a Log Viewer for Large Logs in Svelte - How to build a production-ready log viewer with SvGrid - virtualization for millions of lines, severity coloring, live tailing with scroll-lock, and server-side filtering.
- Lazy-Loading Master-Detail Content in SvGrid - Fetch detail-panel data only when a row is expanded, cache the results, and cancel abandoned requests - keeping a large grid fast without paying for panels no one views.
- First Light - Pointing SvGrid at 100,000 Rows - When we first fed SvGrid a hundred thousand rows, we were testing more than performance - we were testing whether building native on Svelte 5 runes was the right call from the start.
More Layout examples
- Account & security settings console - A real SaaS settings surface composed from the UI kit: SvMenubar app bar, promise + Undo-action toasts on save, SvPopconfirm on destructive rows, SvHoverCard teammate previews, and frame input adornments (leading icons, prefix affixes, masked API key). Profile / Team / Security tabs over SvCard + SvStat.
- Invoice builder - An adornment-heavy money form: currency prefixes, % suffixes, masked tax IDs (frame adornments), line items with SvPopconfirm delete + Undo action toasts, live SvStat totals, an SvHoverCard tax hint, and a promise toast on Send. Pure UI-kit composition.
- Operations KPI dashboard - A KPI console: SvStat + SvSparkline tiles, an SvGauge SLA dial, SvHoverCard drill-down previews per service, an SvMenubar toolbar (View / Range / Actions) and a promise toast on Refresh. Pure UI-kit composition, no grid dependency.
- Form: rich field types - SvForm reaching the whole input suite from one schema - phone, country, mask, combobox, radio, slider, tags, datetime and file - plus per-field help text, a readonly field, column span, and a promise toast on submit.
- Form: cascading fields - SvForm dependent selects - a child list derives from the parent value via a function `options`, and `dependsOn` clears the child when the parent changes so a stale selection never lingers. Country -> State -> City.