Performance & Large Data
Virtualization, 1M rows, server-side data
EliteGrid is designed to stay fast with very large datasets. This chapter explains how, and how to feed data from a server.
Virtualization — how it renders a million rows
The grid only renders the rows that are actually visible in the scroll viewport (plus a small buffer above and below). Scrolling swaps which rows are drawn. This means the DOM stays tiny no matter how many rows you have, so 100, 100,000, or 1,000,000 rows all scroll smoothly.
You don't have to enable anything — virtualization is always on.
The terms behind this:
- DOM ("Document Object Model") is the browser's live, in-memory tree of the HTML elements currently on the page. Every visible element costs memory and time to lay out and paint. A table with a million
<div>rows would mean a million DOM nodes — far too many, and the page would freeze.- Viewport is the visible window onto the data — the scrollable area you can actually see. Typically only a few dozen rows fit at once.
- Buffer is a handful of extra rows rendered just above and below the viewport so that when you scroll, the next rows are already there instead of appearing as a blank flash.
So instead of a million DOM nodes, the grid keeps roughly (visible rows + buffer) nodes — a few dozen — and reuses them as you scroll. That constant, tiny node count is what keeps it fast.
Tuning the scroll buffer
The scroll.bufferSize controls how many extra rows are rendered just outside the viewport. Higher = less chance of a blank flash on fast scrolling, but slightly more DOM.
| Property | Type | Default | Meaning |
|---|---|---|---|
bufferSize |
number |
10 |
Extra rows rendered above & below the viewport |
const grid = createGrid<Row>({
columns,
data: millionRows,
scroll: { bufferSize: 30 }, // smoother fast-scroll for heavy datasets
})
The default of
10is good for most apps. Raise it (e.g. 20–40) if you see blank rows flash during very fast scrolling. Lower it if your cells are heavy to render.
Pagination vs. virtualization
You do not need pagination for performance — virtualization already handles huge datasets. Use pagination only if page-by-page navigation is the UX you want. See Chapter 05.
Server-side data with dataSource
So far we've assumed all your rows are already in the browser (the data array). That works wonderfully up to tens of thousands of rows. But some datasets are simply too big to download in one go — millions of records, or data that changes constantly on the server.
For those cases, provide a dataSource instead of data. This flips the model: instead of you handing the grid everything, the grid asks you for one page at a time, and you fetch just that page from your server. Because the server only sends the rows currently needed, the browser stays light. Sorting, filtering, and paging then happen on the server (it has all the data), and the grid tells your getRows function the current sort/filter/page so your query can honour them.
asyncandPromise(used below). Fetching from a server takes time, sogetRowsis markedasyncand returns aPromise— a placeholder for "a value that will arrive later". Theawaitkeyword pauses inside the function until the network response comes back. The grid shows its loading overlay (see Chapter 09) while it waits.
A data source is any object with a getRows method:
import type { IDataSource, DataSourceParams, DataSourceResult } from '@elitegrid/core'
const serverData: IDataSource<User> = {
async getRows(params: DataSourceParams): Promise<DataSourceResult<User>> {
const { page, pageSize, sortModel, filterModel } = params
const res = await fetch('/api/users?' + new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
sort: JSON.stringify(sortModel),
filter: JSON.stringify(filterModel),
}))
const json = await res.json()
return {
rows: json.rows, // this page's rows
totalCount: json.total, // total rows across all pages
}
},
}
const grid = createGrid<User>({
columns,
dataSource: serverData, // ← use dataSource instead of data
pagination: { enabled: true, pageSize: 50 },
})
DataSourceParams (what the grid sends you):
| Field | Type | Meaning |
|---|---|---|
page |
number |
Which page to fetch |
pageSize |
number |
Rows per page |
sortModel |
SortModel[] |
Current sort columns/directions |
filterModel |
FilterModel |
Current filters |
DataSourceResult (what you return):
| Field | Type | Meaning |
|---|---|---|
rows |
TData[] |
The rows for the requested page |
totalCount |
number |
Total rows (so the grid can show page counts) |
To re-fetch (e.g. after an external change), call api.refreshData().
Performance tips checklist
- ✅ Create the grid once, directly in
<script setup>— not inside acomputedorwatchEffectthat reruns (see Ch. 01). - ✅ Keep
formatterfunctions cheap — they run for every visible cell on every scroll frame. Avoid heavy work or object allocation inside them. - ✅ Set a unique
rowIdso the grid can track rows efficiently. - ✅ For server data, use a
dataSourceso the server does the heavy filtering/sorting. - ✅ Debounce
onScrollChangeif you do work in it — it fires very frequently. - ✅ Selecting one row only re-renders that row, not the whole grid — so selection stays cheap even with huge datasets.
Measuring instead of guessing
Before optimizing anything, confirm there's actually a problem and find where it is:
- Browser DevTools → Performance tab. Record a session while scrolling and interacting with the grid. Smooth scrolling shows up as a steady ~60 frames per second; janky scrolling shows long yellow ("scripting") bars between frames.
- Vue DevTools → Component inspector / Performance tab. Record an interaction (a sort, a filter keystroke) and check which components re-rendered. If components far outside the grid re-render on every keystroke, look for reactive state that's broader than it needs to be (e.g. a shared
reactive()object more components depend on than necessary). - Suspect
formatter/cellClass/customFilterfirst. These run per-cell, per-row respectively, on every relevant render — they're the most common source of a slowdown that looks like "the grid is slow" but is actually "my function is slow." Comment one out temporarily to confirm before rewriting anything.
Common performance mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Typing in a filter box feels laggy | debounceMs too low, or a heavy customFilter/formatter |
Raise debounceMs (see Chapter 04); keep formatters and custom filters cheap |
Grid "freezes" briefly on first load with huge data |
Loading the entire dataset into the browser at once | Switch to a dataSource so only one page is fetched at a time |
| Scrolling shows blank flashes | scroll.bufferSize too low for how fast users scroll |
Raise bufferSize (see above) |
| App feels slow even when the grid is idle | Grid was re-created repeatedly (createGrid called inside a computed/watchEffect that keeps re-running) |
Call createGrid()/useCreateGrid() once, directly in <script setup> — this is the single most common cause of all grid performance complaints |
| Selecting one row re-renders the whole page | Usually a symptom of the grid being rebuilt repeatedly (see above), not a real selection issue | Same fix — once the engine is stable across renders, selection updates stay isolated to the affected row |