# 100k rows × 100 columns

Row + column virtualization. Chunked load with progress + cancellation.

Uses `@svgrid/grid` (MIT). Category: Getting Started. Live demo: https://svgrid.com/demos/06-large-dataset/

Install: `npm i @svgrid/grid` (Svelte 5).

## About this example

Row and column virtualization in the Svelte 5 data grid: 100,000 rows by 100 columns of sales-rep ledgers with monthly revenue, units and margin. Data is generated in chunks with a progress indicator and a cancel button, then the grid renders only the rows and columns in view, so both axes scroll smoothly. Sorting, filtering, cell selection and inline editing stay on at that size.

Row + column virtualization make a wide grid scroll smoothly.

The data is a sales team where each rep carries a rolling monthly ledger - Revenue / Units / Margin per month going back a couple of years. That's a genuinely wide real-world shape (not placeholder "Metric N" columns), so scrolling both axes exercises the virtualizer on data that means something.

The user can scale it up at runtime. The default is 10,000 rows × 55 columns - a realistic enterprise size that mounts in well under a second. The 100,000-row option pushes the grid hard; expect a brief pause on mount, then smooth scrolling once the virtualizer is live.

## Imports, features and API used

- Imports: `@svgrid/grid`, `../shared/seed`
- Table features registered: `rowSortingFeature`, `columnFilteringFeature`

## Frequently asked questions

### How do I enable column virtualization in SvGrid?

Set virtualization for rows and columnVirtualization for columns. overscan and columnOverscan control how many extra rows and columns render outside the viewport; columnWidth gives wide grids a default width so the virtualizer can lay columns out without measuring each one.

### How big can the dataset go?

The demo defaults to 10,000 rows by 55 columns and lets you scale to 100,000 rows at runtime. The 100k option pauses briefly while the data is generated, then scrolls smoothly because the DOM only ever holds the visible slice.

### Why is the data loaded in chunks?

Generating 100,000 rows in one synchronous pass would freeze the tab. The demo builds the array in chunks with a progress bar and a cancel button, which is the same pattern to use when streaming rows from a server.

## Related documentation

- [6. Going to production](https://svgrid.com/docs/getting-started/6-going-to-production.md)
- [Performance benchmarks](https://svgrid.com/docs/help/benchmarks.md)
- [Column sizing](https://svgrid.com/docs/help/columns/column-sizing.md)
- [Row pagination](https://svgrid.com/docs/help/rows/row-pagination.md)
- [Benchmark harness](https://svgrid.com/docs/recipes/benchmark-harness.md)

## Related articles

- [Reactivity with Large Arrays and Objects in Svelte 5](https://svgrid.com/blog/reactivity-large-arrays-objects/) - Deep proxies are powerful but not free. Here is when to pay the cost and when to use $state.raw to keep bulk data loads fast in a SvGrid app.
- [First Light - Pointing SvGrid at 100,000 Rows](https://svgrid.com/blog/first-light-100000-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.
- [Column Virtualization Explained](https://svgrid.com/blog/column-virtualization-explained/) - Row virtualization is well understood, but wide grids expose a second bottleneck. Here is how column virtualization works, when you actually need it, and what trade-offs to expect.
- [Virtual Scrolling Explained](https://svgrid.com/blog/virtual-scrolling-explained/) - A practical look at how windowed rendering keeps large grids fast, what makes it break, and how to verify it's actually working.
- [Performance Tips for SvGrid with Svelte 5 Runes](https://svgrid.com/blog/performance-tips-with-runes/) - Stable references, batched transactions, and $state.raw - the three techniques that keep a 30,000-row live grid at 60 fps without heroics.

## Source code (06-large-dataset.svelte)

```svelte
<script lang="ts">
  /**
   * 06. Large dataset, virtualized
   * ------------------------------
   * Row + column virtualization make a wide grid scroll smoothly.
   *
   * The data is a sales team where each rep carries a rolling monthly ledger -
   * Revenue / Units / Margin per month going back a couple of years. That's a
   * genuinely wide real-world shape (not placeholder "Metric N" columns), so
   * scrolling both axes exercises the virtualizer on data that means something.
   *
   * The user can scale it up at runtime. The default is 10,000 rows × 55
   * columns - a realistic enterprise size that mounts in well under a second.
   * The 100,000-row option pushes the grid hard; expect a brief pause on
   * mount, then smooth scrolling once the virtualizer is live.
   */
  import {
    SvGrid,
    tableFeatures,
    rowSortingFeature,
    columnFilteringFeature,
    type ColumnDef,
    type GridColumns,
  } from '@svgrid/grid'
  import { makeWidePeople, metricKind, type WidePerson } from '../shared/seed'

  const features = tableFeatures({
    rowSortingFeature,
    columnFilteringFeature,
  })

  // `cols` is the number of monthly-KPI columns; the label counts the 5 fixed
  // identity columns too. The smaller tiers keep cols a multiple of 3 so months
  // group cleanly; the top tier fills to a round 100 columns (the last month is
  // partial, which the KPI-cycling handles fine).
  type Size = { rows: number; cols: number; label: string }
  const sizes: Size[] = [
    { rows: 1_000,   cols: 24, label: '1k × 29' },
    { rows: 10_000,  cols: 48, label: '10k × 53' },
    { rows: 50_000,  cols: 72, label: '50k × 77' },
    { rows: 100_000, cols: 95, label: '100k × 100' },
  ]

  let size = $state<Size>(sizes[1]!)
  let busy = $state(false)
  let rows = $state.raw<WidePerson[]>([])
  let columns = $state.raw<ColumnDef<typeof features, WidePerson>[]>([])
  let mountedAt = $state(0)

  // Month labels for the rolling ledger, newest first: "Aug '26", "Jul '26"…
  // One label per calendar month back from today.
  const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
  function monthLabel(back: number): string {
    const now = new Date()
    const d = new Date(now.getFullYear(), now.getMonth() - back, 1)
    return `${MONTHS[d.getMonth()]} '${String(d.getFullYear()).slice(-2)}`
  }

  // Each wide column is one month of one KPI, cycling Revenue / Units / Margin.
  // The header and number format follow the KPI, so the grid reads like a real
  // performance ledger rather than "Metric 0 … Metric 94".
  const KPI = [
    { name: 'Revenue', format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } } },
    { name: 'Units',   format: { type: 'number', options: { maximumFractionDigits: 0 } } },
    { name: 'Margin',  format: { type: 'percent', valueIsPercentPoints: true, options: { maximumFractionDigits: 1 } } },
  ] as const

  function buildColumns(metrics: number): GridColumns<WidePerson> {
    const base: GridColumns<WidePerson> = [
      { field: 'firstName',  header: 'First name', editorType: 'text', width: 140 },
      { field: 'lastName',   header: 'Last name',  editorType: 'text', width: 140 },
      { field: 'department', header: 'Team',       editorType: 'text', width: 130 },
      { field: 'country',    header: 'Region',     editorType: 'text', width: 100 },
      { field: 'status',     header: 'Status',     editorType: 'text', width: 110 },
    ]
    const ledger: GridColumns<WidePerson> = []
    for (let i = 0; i < metrics; i++) {
      const kpi = KPI[metricKind(i)]!
      // Three KPIs share each month, so month N advances every 3 columns.
      const month = monthLabel(Math.floor(i / 3))
      ledger.push({
        field: `metric_${i}` as `metric_${number}`,
        header: `${month} · ${kpi.name}`,
        editorType: 'number',
        format: kpi.format,
        width: 150,
      })
    }
    return [...base, ...ledger]
  }

  async function load(next: Size) {
    busy = true
    // Unmount the grid first so the heavy old rows are GC'd before the new ones
    // are generated. Without this, peak memory is ~2× the larger size.
    rows = []
    columns = []
    await new Promise((r) => requestAnimationFrame(r))
    const t0 = performance.now()
    const generated = makeWidePeople(next.rows, next.cols, 1337)
    columns = buildColumns(next.cols)
    rows = generated
    size = next
    mountedAt = Math.round(performance.now() - t0)
    busy = false
  }

  // Initial load
  $effect(() => {
    if (rows.length === 0 && !busy) load(size)
  })
</script>

<section class="flex flex-col flex-1 min-h-0 gap-3">
  <div class="flex flex-wrap items-center gap-2 text-sm shrink-0">
    <span class="font-medium">Dataset:</span>
    {#each sizes as option (option.label)}
      {@const active = option.rows === size.rows && option.cols === size.cols}
      <button
        type="button"
        onclick={() => load(option)}
        disabled={busy || active}
        class="rounded border px-3 py-1 ds-btn {active ? 'ds-btn-on font-semibold' : ''} disabled:opacity-50"
      >{option.label}</button>
    {/each}
    <span class="ml-auto ds-meta">
      {#if busy}
        Generating…
      {:else if rows.length}
        {size.rows.toLocaleString()} rows · {size.cols + 5} columns · generated in {mountedAt} ms
      {/if}
    </span>
  </div>

  {#if rows.length}
    <div class="flex-1 min-h-0">
      <SvGrid responsive={true}
        columnResize
        data={rows}
        columns={columns}
        features={features}
        filterMode="menu"
        selectionMode="cell"
        enableInlineEditing={false}
        enableCellSelection={true}
        showRowNumbers={true}
        virtualization={true}
        columnVirtualization={true}
        rowHeight={32}
        overscan={8}
        columnOverscan={3}
        columnWidth={150}
        containerHeight="100%"
      />
    </div>
  {/if}
</section>

<style>
  /* Toolbar chrome follows the active grid theme via --sg-* tokens. */
  .ds-btn {
    border-color: var(--sg-border, #cbd5e1);
    background: var(--sg-bg, transparent);
    color: var(--sg-fg, inherit);
  }
  .ds-btn:not(.ds-btn-on):hover:not(:disabled) { background: var(--sg-row-hover-bg, transparent); }
  .ds-btn-on { background: var(--sg-bg-subtle, var(--sg-header-bg, #e2e8f0)); }
  .ds-meta { color: var(--sg-muted, #64748b); }
</style>
```

[View this example on GitHub](https://github.com/sv-grid/sv-grid/blob/main/examples/src/demos/06-large-dataset.svelte)
