fix: Avoid redundant table column measurements on re-render - #4609
Conversation
Memoize the visible column definitions and the width/id arrays derived from them in InternalTable. These were rebuilt as fresh arrays on every render, so the reference passed to ColumnWidthsProvider always changed. That re-triggered the provider's width-sync effect, which calls getBoundingClientRect() on every render even when columns are unchanged (e.g. typing in the filter box), contributing to interaction lag. With the columns memoized, the effect only re-runs when the columns actually change. Adds tests asserting that re-rendering with unchanged columns performs no DOM measurements, while changing columns still does.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4609 +/- ##
=======================================
Coverage 97.60% 97.60%
=======================================
Files 951 951
Lines 30730 30742 +12
Branches 11277 11278 +1
=======================================
+ Hits 29993 30005 +12
Misses 730 730
Partials 7 7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ernst-dev
left a comment
There was a problem hiding this comment.
Looks like there's a valid failing test relating to table measurements, please take a look at the failure
Memoizing visibleColumns stopped the ColumnWidthsProvider effect from re-firing on data-only re-renders. For auto-layout columns, changing the data (pagination/filtering) changes the rendered column widths, so the sticky header copies were left with stale widths. Observe the primary header cells with a ResizeObserver and re-sync the sticky copies when their widths actually change. This restores correctness while keeping the optimization: fixed-width and resizable columns don't resize on data changes, so the observer stays idle.
Head branch was pushed to by a user without write access
Fixed! |
ernst-dev
left a comment
There was a problem hiding this comment.
Sorry Trevor, I know this is annoying but can we hit the codecov target for these changes?
Done! |
This PR reduces unnecessary DOM measurements when a table with "sticky" features (
stickyColumnsorstickyHeader) re-renders.ColumnWidthsProviderruns auseEffectthat depends on itsvisibleColumnsarray. That effect callsupdateColumnWidths(), which synchronizes sticky cells by reading layout from the DOM viagetBoundingClientRect()(thestickybranch ofgetColumnStyles). InInternalTable, the arrays passed to the provider —visibleColumnDefinitionsand the derivedvisibleColumnWidthsWithSelection/visibleColumnIdsWithSelection— are rebuilt as fresh arrays of fresh objects on every render, causing the provider's effect to re-fire. These unnecessary calculations contribute to input lag when users type in a sticky table's searchbox.Memoize the derivations in
InternalTable:visibleColumnDefinitions— keyed oncolumnDefinitions,columnDisplay, andvisibleColumns.visibleColumnWidthsWithSelection/visibleColumnIdsWithSelection— keyed onhasSelectionandvisibleColumnDefinitions.Re-sync sticky headers on content-driven resizes:
Memoizing
visibleColumnsmeans the provider's effect no longer re-fires on every render, so sticky header copies are no longer re-synced from the primary cells on unrelated re-renders. That's the point for fixed-width and resizable columns, but auto-layout columns (no explicit width) can change width when their content changes without any change to the column definitions (e.g. paginating or filtering). To keep the sticky header copies correct in that case,ColumnWidthsProvidernow observes the primary cells with aResizeObserverand re-syncs the copies when a primary cell resizes. The observer re-attaches when the set of visible columns changes, and no-ops in environments withoutResizeObserver(e.g. SSR).Benefits
Measured
getBoundingClientRectcalls per re-render with unchanged columns:stickyColumnsstickyHeaderThe remaining reads come from
StickyHeader's own per-render synchronization, a separate mechanism this PR does not change (a candidate for a follow-up).Non-sticky tables benefit in a smaller way from reduced array allocations.
How has this been tested?
Added unit tests in
src/table/__tests__/columns-width.test.tsx:does not measure column widths when re-rendering with unchanged columns— re-renders with newitemsbut identicalcolumnDefinitionsand asserts nogetBoundingClientRectcalls occur. Fails onmain, passes with this change.does measure column widths when the columns change— a negative control that re-renders with an added column and asserts measurement still occurs.re-syncs the sticky header copy when a primary cell changes width without a column change— simulates an auto-layout cell growing and asserts the observed resize re-syncs the sticky copy.renders without a ResizeObserver (e.g. server-side rendering)— asserts the provider renders without throwing whenResizeObserveris absent.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.