V4 V5

Creating a 1,048,576 x 16,384 sheet uses almost no memory until you write a value into it. V5 only pays for cells that exist, so speed is decided by how many cells you actually wrote and how many are on screen — not by the sheet’s dimensions.

This page is about writing code that takes advantage of that.

What you can rely on

#Characteristic
1Empty cells cost nothing. A hundred full-size sheets use almost no memory while empty
2About 16-23 bytes per cell. Values are a 16-byte struct, with no boxing
3Identical formatting collapses into one. The same style on tens of thousands of cells exists once
4Identical strings collapse into one. A repeated string is shared within the sheet
5Rows and columns store only differences. A row whose height you never changed has no entry
6Iteration is proportional to the cells that exist. Empty cells are not visited
7Drawing and interaction are proportional to what is visible. Sheet dimensions do not matter

These are guaranteed. Write code that assumes them.

Clamp iteration to the used range

The most common performance problem is naively looping from 0 to RowCount. Reading an empty cell is cheap in itself, but a million iterations is not.

using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.Style;

// the good way - clamp to the used range before looping
if (ws.TryGetUsedRange(out var used))
{
	for (int r = used.Row; r <= used.EndRow; r++)
		for (int c = used.Col; c <= used.EndCol; c++)
			_ = ws.GetValue(r, c);
}

TryGetUsedRange returns the range that holds a value, a formula or a style. On an empty sheet it returns false.

Walk sparse data with ReadWindow

When the used range is wide but the content is scattered (one filled row every ten thousand, say), ReadWindow is clearly faster. Only cells that exist reach the callback; empty ones never do.

// better still - only cells that exist are visited (empty ones never call back)
ws.ReadWindow(0, 0, ws.RowCount, ws.ColumnCount, (row, col, value, styleId) =>
{
	_ = value;
});

Rendering, XLSX export and PDF export all use this shape. Consider it first when writing your own aggregation or export.

Whole-row and whole-column formatting goes on the row or column

// the good way - a column's default style is a single entry
ws.SetColumnStyle(2, new StyleRecord { TextAlign = HAlign.Right });

Writing the same thing in a cell loop materializes one entry per cell in that column. It is the most common way to lose characteristic 1.

The result looks identical but memory and time do not. See Style Inheritance.

You do not need to reuse style objects

Identical styles are merged automatically, so there is no need to hoist a StyleRecord into a variable. Even a fresh new on every iteration shares one instance.

// styles with the same content collapse into one (interned by record value equality)
for (int r = 0; r < 10_000; r++)
	ws.SetCellStyle(r, 0, new StyleRecord { Bold = true });

int distinct = ws.DistinctStyleCount;   // goes up by exactly one

DistinctStyleCount is useful for diagnosis. When it is larger than you expect, look for styles that differ slightly per cell (one cell with a font half a point bigger, say).

Bulk-loading data

When writing tens of thousands of rows, this order is fastest.

  1. Decide sizes and formatting first, per row and column (ws.Rows.SetSize / ws.Columns.SetSize / SetRowStyle / SetColumnStyle)
  2. Write values (SetNumber / SetText are the lightest; SetObjectValue pays for type dispatch)
  3. Write formulas

SetFormula recalculates that cell and everything depending on it, right then. When loading a table whose formulas depend on each other, writing the depended-on cells first reduces how often that happens. If you would rather not think about order, write all the values first, then the formulas, and call ws.Recalculate() once at the end.

Setting RowDataSource lets rows be prepared only when they are displayed — useful for showing a database result of several million rows.

ws.RowDataSource = new MyRowSource();   // implement IRowDataSource
ws.EnsureRowsLoaded(0, 99);             // only when you want to pre-load explicitly

The control calls EnsureRowsLoaded for the visible window before every paint. Rows already prepared are skipped, so being called every frame is not a problem.

To open a huge xlsx, the file side can page in too — OpenVirtual, in XLSX.

Patterns that destroy the characteristics

Do notWhat happensInstead
for (int r = 0; r < ws.RowCount; r++)a million iterationsclamp with TryGetUsedRange, or ReadWindow
Set a whole column’s style cell by cellone entry per cellSetColumnStyle
Write values to “initialize” empty cellscharacteristic 1 is lostdo not; leave them empty
Create slightly different styles per cellmerging cannot helpkeep the number of variants small
Recalculate() inside a loopthe whole sheet recalculates each timeonce, after you finish

Measuring

For real memory and allocation numbers, use the benchmark project.

dotnet run -c Release --project ReoGrid.Core.Bench

From inside your own app, the quickest check is ws.DistinctStyleCount and the range ws.TryGetUsedRange gives back. If either is larger than expected, you are hitting one of the rows in the table above.

Was this article helpful?