Skip to content

fix: Avoid redundant table column measurements on re-render - #4609

Merged
ernst-dev merged 14 commits into
cloudscape-design:mainfrom
TrevorBurnham:perf/memoize-table-column-widths
Jul 20, 2026
Merged

fix: Avoid redundant table column measurements on re-render#4609
ernst-dev merged 14 commits into
cloudscape-design:mainfrom
TrevorBurnham:perf/memoize-table-column-widths

Conversation

@TrevorBurnham

@TrevorBurnham TrevorBurnham commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

This PR reduces unnecessary DOM measurements when a table with "sticky" features (stickyColumns or stickyHeader) re-renders.

ColumnWidthsProvider runs a useEffect that depends on its visibleColumns array. That effect calls updateColumnWidths(), which synchronizes sticky cells by reading layout from the DOM via getBoundingClientRect() (the sticky branch of getColumnStyles). In InternalTable, the arrays passed to the provider — visibleColumnDefinitions and the derived visibleColumnWidthsWithSelection / 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 on columnDefinitions, columnDisplay, and visibleColumns.
  • visibleColumnWidthsWithSelection / visibleColumnIdsWithSelection — keyed on hasSelection and visibleColumnDefinitions.

Re-sync sticky headers on content-driven resizes:

Memoizing visibleColumns means 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, ColumnWidthsProvider now observes the primary cells with a ResizeObserver and 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 without ResizeObserver (e.g. SSR).

Benefits

Measured getBoundingClientRect calls per re-render with unchanged columns:

Configuration Before After
stickyColumns 3 0
stickyHeader 5 3

The 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 new items but identical columnDefinitions and asserts no getBoundingClientRect calls occur. Fails on main, 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 when ResizeObserver is absent.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

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

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.60%. Comparing base (82568c5) to head (c6a0d0a).

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.
📢 Have feedback on the report? Share it here.

🚀 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.

@ernst-dev
ernst-dev enabled auto-merge July 9, 2026 08:38

@ernst-dev ernst-dev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there's a valid failing test relating to table measurements, please take a look at the failure

ernst-dev and others added 2 commits July 10, 2026 12:07
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.
auto-merge was automatically disabled July 11, 2026 03:02

Head branch was pushed to by a user without write access

@TrevorBurnham

Copy link
Copy Markdown
Contributor Author

Looks like there's a valid failing test relating to table measurements, please take a look at the failure

Fixed!

@pan-kot
pan-kot requested a review from ernst-dev July 13, 2026 04:50

@ernst-dev ernst-dev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry Trevor, I know this is annoying but can we hit the codecov target for these changes?

@TrevorBurnham

Copy link
Copy Markdown
Contributor Author

Sorry Trevor, I know this is annoying but can we hit the codecov target for these changes?

Done!

@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react16 July 20, 2026 09:42 Inactive
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react18 July 20, 2026 09:42 Inactive
@ernst-dev
ernst-dev enabled auto-merge July 20, 2026 12:22
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react16 July 20, 2026 12:34 Inactive
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react18 July 20, 2026 12:35 Inactive
@ernst-dev
ernst-dev added this pull request to the merge queue Jul 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jul 20, 2026
@ernst-dev
ernst-dev enabled auto-merge July 20, 2026 14:01
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react16 July 20, 2026 14:14 Inactive
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react18 July 20, 2026 14:15 Inactive
@ernst-dev
ernst-dev added this pull request to the merge queue Jul 20, 2026
Merged via the queue into cloudscape-design:main with commit c8a9b3b Jul 20, 2026
52 checks passed
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