Skip to content

Give calculated columns a single shape to avoid megamorphic property reads - #4222

Merged
nstepien merged 5 commits into
mainfrom
stable-column-shapes
Oct 22, 2026
Merged

nstepien merged 5 commits into
mainfrom
stable-column-shapes

Conversation

@nstepien

@nstepien nstepien commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Applies What's up with monomorphism? and Elements kinds in V8 to the grid. Only the first one called for changes.

Cause

useCalculatedColumns created each calculated column with { ...rawColumn, … }, so the column's hidden class (shape) depended on which optional properties the raw column set, and in what order. With the columns of the CommonFeatures demo, the 16 calculated columns had 9 different shapes. V8's inline caches handle up to 4 shapes per property access, so the column.* reads that run for every rendered cell, in Cell, getColSpan, getCellStyle, getCellClassname, isCellEditableUtil, and others, went megamorphic.

Changes

  • Calculated columns set every property in a fixed order, undefined when unset. To match, CalculatedColumn redeclares the optional Column properties as required, with Column's own property types, like Column<TRow, TSummaryRow>['cellClass'], so leaving one out is a type error. Unlike Required<Column>, this keeps undefined in their types whether or not apps enable exactOptionalPropertyTypes.
  • setPosition builds the active position from idx and rowIdx instead of spreading the position it receives, and the five position literals written as { rowIdx, idx } now use { idx, rowIdx } like the others.
  • AGENTS.md documents the convention.

Measurements

Measured on the packed lib/ with Node 26 (V8 14.6), using the CommonFeatures columns and enableVirtualization={false}:

Before After
Shapes among the 16 calculated columns (%HaveSameMap) 9 1
Load sites that read calculated column properties (--log-ic) 34 megamorphic, 2 polymorphic, and 3 monomorphic all 39 monomorphic
renderToString of 300 rows × 16 columns, median 11.04 ms 10.74 ms

The render time improves by 2.7%, across 8 alternating pairs of processes. React's own work dominates rendering, and DOM work in the browser dilutes the gain further, so this is a small, steady improvement rather than a big one.

Reads of the raw columns stay megamorphic, since users create them, but they only run in useMemo when columns changes.

Behavior and type changes

  • Calculated columns now have every Column property as an own property, so Object.keys(column) and 'cellClass' in column return different results than before.
  • Properties that aren't part of Column are no longer copied to calculated columns, as they aren't supported.
  • Every Column property is required on CalculatedColumn, so code that creates CalculatedColumn objects itself, like test mocks, has to set all of them.

Elements kinds

No changes needed. The array patterns the grid uses all create packed arrays on V8 14.6, and no hot loop reads past the end of an array.

Auditing the out-of-bounds reads found a crash, which #4223 fixes.

Verification

  • A new test checks that all calculated columns have the same properties in the same order. It fails before this change.
  • vp run ci passes.

🤖 Generated with Claude Code

…reads

`useCalculatedColumns` spread each raw column into its calculated column,
so the hidden class of each column depended on which optional properties
were set, and in what order. Past 4 shapes, the column property reads that
run for every rendered cell go megamorphic.

Calculated columns now set every property in a fixed order, and copy
custom properties after the known ones. `setPosition` builds the active
position from `idx` and `rowIdx` instead of spreading its input, and all
position literals use the `{ idx, rowIdx }` order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Oct 22, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.56%. Comparing base (e36fd41) to head (3bcf373).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4222   +/-   ##
=======================================
  Coverage   98.56%   98.56%           
=======================================
  Files          45       45           
  Lines        1956     1957    +1     
  Branches      728      728           
=======================================
+ Hits         1928     1929    +1     
  Misses         28       28           
Files with missing lines Coverage Δ
src/Cell.tsx 100.00% <100.00%> (ø)
src/DataGrid.tsx 98.88% <100.00%> (+<0.01%) ⬆️
src/GroupedRow.tsx 100.00% <100.00%> (ø)
src/SummaryCell.tsx 100.00% <100.00%> (ø)
src/hooks/useCalculatedColumns.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

nstepien added a commit that referenced this pull request Oct 22, 2026
Found while working on #4222. This fixes two bugs with PageUp and
PageDown on header and summary rows.

### Crash without rows

In a grid with no rows and a `rowHeight` function, PageUp and PageDown
on header and summary rows could throw:

```
TypeError: Cannot read properties of undefined (reading 'top')
```

With a `rowHeight` function, `useViewportRows` reads row positions from
its `rowPositions` array. `getRowTop` and `getRowHeight` clamp the row
index to the first or last row, so header and summary rows use the
position of the closest row. With no rows, the clamp returned index 0 of
an empty array, and reading `.top` or `.height` from `undefined` threw.
A number `rowHeight` computes positions from the index, so it wasn't
affected.

`getRowTop` and `getRowHeight` now return an empty position at the top
(`top: 0`, `height: 0`) when there are no rows, so PageUp and PageDown
move the active cell like they do with a number `rowHeight`.

### PageUp moving down

On a top summary row, or on the main header row when column group header
rows are above it, PageUp moved the active cell down to the first row.
PageUp aimed above the first row, and `nextRowY > 0 ?
findRowIdx(nextRowY) : 0` turned that into row 0. PageUp only did
nothing on the first header row.

PageUp now does nothing on any row above the rows (`rowIdx < 0`), like
PageDown already does on rows below them (`rowIdx >= rows.length`).

### Verification

- Each new test failed in Chromium before its fix: the crash test with
the `TypeError` reported as an unhandled error, and the PageUp tests
with the active cell moved to the first row.
- With the PageUp fix but without the `useViewportRows` fix, the crash
test still fails, since PageDown on the header row and PageUp on a
bottom summary row still reach `getRowTop`.
- The crash test also passes with `rowHeight: 35`, so both kinds of
`rowHeight` navigate the same way.
- `vp run ci` passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
nstepien and others added 3 commits October 23, 2026 00:01
Columns don't support properties outside of `Column`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calculated columns set every property, so the type now says so, and
leaving a property out of the object literal in `useCalculatedColumns`
is a type error, including properties added to `Column` later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nstepien
nstepien marked this pull request as ready for review October 22, 2026 23:20
Apps type-check the published declarations with their own compiler
options. Without `exactOptionalPropertyTypes`, which is off by default,
`Required<Column>` removed `undefined` from the optional properties, so
`renderEditCell` was typed as a function or `null` while it's `undefined`
when not set. Redeclaring them as required with `Column`'s own property
types keeps `undefined` with either setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nstepien
nstepien merged commit e630880 into main Oct 22, 2026
7 of 8 checks passed
@nstepien
nstepien deleted the stable-column-shapes branch October 22, 2026 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants