Performance
The grid draws what is on screen and nothing else, so the cost of a frame follows the visible area, not the number of rows. What remains in your hands is the code the grid calls while it draws, and how your data changes.
What does not grow with the row count
- Drawing. No control is created per row. The measured frame cost at 1,000 and at 1,000,000 rows is on the documentation start page; on both hosts the two differ by 0.05 ms.
- Scrolling. On the CPU path the grid shifts the picture and repaints only the newly exposed strip; on the GPU path it reuses what it can.
- Selection. Select-all and ranges are stored as intervals, not as a list of rows.
- Repainting after a change. Hover, an edit or a changed item repaints the affected rows, not the grid.
What does
- Sorting and filtering visit every row when the query changes — once, not per frame. They reorder and select indexes and never copy an item, which is why a million rows sort without a second million objects.
- Grouping and pivot (Pro) bucket every row when they are rebuilt. A pivot over live data re-aggregates at most once per
FollowInterval. - The value list of a filter collects distinct values over the column the first time it opens; its display is capped, however many values there are.
- Column auto-size measures the column's text.
Keep column code cheap
Text getters, style predicates, tooltips and composed cells run while a frame is drawn, for the cells in view, and again on the next frame. On the WPF host that happens on the grid's own render thread.
- Return values that already exist; format, do not compute. No I/O, no locks, no lookups in large collections per cell.
- Do not read UI-thread state from them and do not change anything. Change data on the UI thread.
- Prefer
TypedColumn.NumberandTypedColumn.Dateover text withToString: sorting and filtering then compare the value instead of parsing strings. - A
StyleRulecosts per visible cell, not per row of the data. A rule that needs a statistic over all rows (top 10, above average) belongs in conditional formatting, which computes it once per query.
Live data
- Mutate, do not replace. An
ObservableCollectionbehindObservableItemsSourcelets the grid apply exactly what changed. ReplacingItemsSourceper update rebuilds the projection every time. - Batch bulk changes with
BatchUpdates(): one structural change at the end instead of one per row. - Feeds that send whole objects belong in a
KeyedItemsSource; an upsert finds its row by key instead of by a scan. - Settle row moves in a sorted live grid with
SettleIntervalMs; values still update at once. - Bound logs with
RingItemsSource. It trims in blocks, so a busy stream does not pay a full frame for every row that falls off. - Push on the grid's clock with
Api.Timing.Everyrather than a dispatcher timer, whose ticks arrive in bursts while the grid renders continuously.
Changes are collected and applied once per frame, so a hundred property changes between two frames cost one reaction.
Your own source
An IItemsSource is asked for the items in view on every frame. GetItem must be cheap and constant-time — an array or list index, not a query — and return null for an index out of range rather than throw. For data that is not in memory, use VirtualItemsSource or RemoteItemsSource; both fetch pages in the background and never block GetItem.
Row heights
Uniform rows need no bookkeeping at all. Variable heights are kept as prefix sums, or in a Fenwick tree for rows measured as they scroll into view, so a changed height costs O(log n) rather than a scan. Wrapped text (WithWrap) does not grow the row; pick a row height that fits.
Measuring
- Measure a Release build.
- Check which adapter drew (
RendererName) and, on a laptop with two GPUs, setARION_GPU— see Hosting and rendering. FrameRenderedreports each frame's duration and the number of rows drawn;ProfileFrames = trueadds a breakdown by phase. A frame that draws far more rows than are visible points at invalidation from your code — usually a refresh call per change that the grid would have handled by itself.
