Client-Side vs Server-Side Data for Tables - SvGrid blog illustration

Client-Side vs Server-Side Data for Tables

The architectural fork that determines your grid's performance ceiling - how to pick the right data mode and wire it correctly in SvGrid.

Most grid performance problems are not really grid problems. They are data-loading decisions made early in the project that nobody revisited. Whether your rows live entirely in the browser or arrive one page at a time from the server shapes latency, memory, security posture, and how complex your data pipeline becomes.

Server-side data in SvGrid. Server-side data in SvGrid.

The choice has a surprisingly clean decision boundary. Understanding it once saves you from debugging symptoms that are actually architectural mismatches.

When all rows belong in the browser

Client-side means you fetch the full dataset once - on component mount or via a store - and hand the array directly to the grid. SvGrid's internal pipeline handles sorting, filtering, and pagination entirely in memory.

<script lang="ts">
  import SvGrid from '@svgrid/grid'
  import {
    tableFeatures,
    rowSortingFeature,
    columnFilteringFeature,
    rowPaginationFeature,
  } from '@svgrid/grid'
  import type { ColumnDef } from '@svgrid/grid'

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

  let rows = $state<Product[]>([])

  $effect(() => {
    fetch('/api/products').then(r => r.json()).then(d => { rows = d })
  })

  const columns: ColumnDef<typeof features, Product>[] = [
    { id: 'name',     field: 'name',     header: 'Name',     width: 220 },
    { id: 'category', field: 'category', header: 'Category', width: 140 },
    { id: 'price',    field: 'price',    header: 'Price',    width: 100, type: 'number' },
    { id: 'stock',    field: 'stock',    header: 'Stock',    width: 90,  type: 'number' },
  ]
</script>

<SvGrid
  data={rows}
  {columns}
  {features}
  sortable
  filterable
  pageable
  showFilterRow={true}
  rowHeight={34}
  virtualization={true}
/>

This mode works well up to tens of thousands of rows. With virtualization enabled, SvGrid only renders the visible viewport regardless of how many rows are in memory - so 50,000 rows will scroll just as smoothly as 500. The filter and sort interactions are instant because there is no network round trip; the grid recomputes the visible slice on every keypress.

The obvious trade-off: the full dataset lands in the browser. This means slower initial load for large tables, higher memory usage, and - critically - all the data is visible in the network tab. If rows contain personal data or pricing your competitors should not see, client-side is the wrong choice regardless of row count.

When the server must stay in control

Server-side mode inverts the architecture. The grid tells your backend what the user wants - which page, which sort order, which active filters - and the backend returns only the matching rows plus a total count. The grid never sees rows it is not displaying.

SvGrid provides createServerDataSource to wire this up without managing the callback plumbing yourself:

import { createServerDataSource } from '@svgrid/grid'

const ds = createServerDataSource({
  fetch: async ({ page, pageSize, sort, filters }) => {
    const params = new URLSearchParams({
      page:     String(page),
      pageSize: String(pageSize),
    })

    for (const s of sort) {
      params.append('sort', `${s.id}:${s.desc ? 'desc' : 'asc'}`)
    }

    for (const [col, f] of Object.entries(filters)) {
      params.append('filter', `${col}:${f.operator}:${f.value}`)
      if (f.valueTo) params.append('filter', `${col}:to:${f.valueTo}`)
    }

    const res  = await fetch(`/api/products?${params}`)
    const json = await res.json()

    return { rows: json.data, total: json.total }
  },
})

Then pass the data source exactly the same way you would pass an array:

<SvGrid
  data={ds}
  {columns}
  {features}
  sortable
  filterable
  pageable
  showFilterRow={true}
  rowHeight={34}
/>

The grid UI is identical - same filter row, same column headers, same pager. Only the data source changes. SvGrid automatically debounces filter input and cancels in-flight requests when newer ones supersede them.

This mode is appropriate when:

The cost is real: every sort column added, every filter typed, every page turn triggers a network request. You need your API to support pagination and filtering parameters, and you need the backend to return an accurate total so the pager knows how many pages exist. On slow or unreliable connections this creates visible loading states between interactions.

The numbers that shift the decision

There is no universal row count where you must switch modes, but some practical brackets hold across most projects:

Row count Recommendation
Under 5,000 Client-side, do not overthink it
5,000 - 50,000 Client-side with virtualization, monitor initial load time
50,000 - 200,000 Client-side is often still fine if data is not sensitive; benchmark
Over 200,000 Server-side, the transfer alone exceeds what most users will tolerate

Sensitivity overrides count. A table of 2,000 customer records with PII should probably be server-side from day one even though 2,000 rows is trivially small for client-side.

Mixing modes: server paging with client-side operations

There is a middle ground worth knowing: fetch pages from the server but keep the current page in memory for faster secondary interactions. This is implicit in how createServerDataSource works - within a loaded page, SvGrid applies client-side sorting if you add secondary sort columns, and client-side row selection works without re-fetching.

You can also load a capped dataset up-front and treat it as client-side. If your API supports a "fetch first N rows" endpoint, pulling 10,000 rows at startup and going fully client-side from there is sometimes the right call. You get fast interactions after the initial load and avoid building a filter/sort API on the backend.

The practical answer

Start with client-side. It is less code, less API surface to build, and instant interactions reward users immediately. Reach for server-side when one of these becomes true:

Switching from client-side to server-side later is straightforward in SvGrid - you replace the array prop with a createServerDataSource result and add the filter/sort API to your backend. The component template does not change. That migration path means you are not locked into an early decision; you can ship client-side and move when the data demands it.