From 4bc6c005aff2c45ced74e433728fee8e71f138d0 Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Wed, 31 Dec 2025 12:09:05 -0500 Subject: [PATCH 1/3] fix: Use Map for O(1) column lookups in table width calculations --- src/table/use-column-widths.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/table/use-column-widths.tsx b/src/table/use-column-widths.tsx index db41a79c8c..6987c2af2e 100644 --- a/src/table/use-column-widths.tsx +++ b/src/table/use-column-widths.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; +import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useResizeObserver, useStableCallback } from '@cloudscape-design/component-toolkit/internal'; import { getLogicalBoundingClientRect } from '@cloudscape-design/component-toolkit/internal'; @@ -35,12 +35,12 @@ function readWidths( } function updateWidths( - visibleColumns: readonly ColumnWidthDefinition[], + columnById: Map, oldWidths: Map, newWidth: number, columnId: PropertyKey ) { - const column = visibleColumns.find(column => column.id === columnId); + const column = columnById.get(columnId); let minWidth = DEFAULT_COLUMN_WIDTH; if (typeof column?.width === 'number' && column.width < DEFAULT_COLUMN_WIDTH) { minWidth = column?.width; @@ -83,6 +83,12 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain const containerWidthRef = useRef(0); const [columnWidths, setColumnWidths] = useState>(null); + // Pre-build a Map for column lookups + const columnById = useMemo( + () => new Map(visibleColumns.map(column => [column.id, column])), + [visibleColumns] + ); + const cellsRef = useRef(new Map()); const stickyCellsRef = useRef(new Map()); const getCell = (columnId: PropertyKey): null | HTMLElement => cellsRef.current.get(columnId) ?? null; @@ -96,7 +102,7 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain }; const getColumnStyles = (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => { - const column = visibleColumns.find(column => column.id === columnId); + const column = columnById.get(columnId); if (!column) { return {}; } @@ -190,7 +196,7 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain }, []); function updateColumn(columnId: PropertyKey, newWidth: number) { - setColumnWidths(columnWidths => updateWidths(visibleColumns, columnWidths ?? new Map(), newWidth, columnId)); + setColumnWidths(columnWidths => updateWidths(columnById, columnWidths ?? new Map(), newWidth, columnId)); } return ( From e74ce8288a029c934c28737b35ab8399bce37085 Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Wed, 31 Dec 2025 12:50:36 -0500 Subject: [PATCH 2/3] fix: Memoize context provider values --- src/internal/components/dropdown/context.tsx | 5 +- src/table/use-column-widths.tsx | 88 +++++++++++-------- .../parts/overflow-menu/router.tsx | 5 +- 3 files changed, 58 insertions(+), 40 deletions(-) diff --git a/src/internal/components/dropdown/context.tsx b/src/internal/components/dropdown/context.tsx index 610cc48396..4452740382 100644 --- a/src/internal/components/dropdown/context.tsx +++ b/src/internal/components/dropdown/context.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; @@ -14,7 +14,8 @@ export interface DropdownContextProviderProps { } export function DropdownContextProvider({ children, position = 'bottom-right' }: DropdownContextProviderProps) { - return {children}; + const value = useMemo(() => ({ position }), [position]); + return {children}; } export function useDropdownContext() { diff --git a/src/table/use-column-widths.tsx b/src/table/use-column-widths.tsx index 6987c2af2e..002c512682 100644 --- a/src/table/use-column-widths.tsx +++ b/src/table/use-column-widths.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useResizeObserver, useStableCallback } from '@cloudscape-design/component-toolkit/internal'; import { getLogicalBoundingClientRect } from '@cloudscape-design/component-toolkit/internal'; @@ -92,47 +92,50 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain const cellsRef = useRef(new Map()); const stickyCellsRef = useRef(new Map()); const getCell = (columnId: PropertyKey): null | HTMLElement => cellsRef.current.get(columnId) ?? null; - const setCell = (sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => { + const setCell = useCallback((sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => { const ref = sticky ? stickyCellsRef : cellsRef; if (node) { ref.current.set(columnId, node); } else { ref.current.delete(columnId); } - }; + }, []); - const getColumnStyles = (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => { - const column = columnById.get(columnId); - if (!column) { - return {}; - } + const getColumnStyles = useCallback( + (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => { + const column = columnById.get(columnId); + if (!column) { + return {}; + } - if (sticky) { - return { - width: - cellsRef.current.get(column.id)?.getBoundingClientRect().width || - (columnWidths?.get(column.id) ?? column.width), - }; - } + if (sticky) { + return { + width: + cellsRef.current.get(column.id)?.getBoundingClientRect().width || + (columnWidths?.get(column.id) ?? column.width), + }; + } - if (resizableColumns && columnWidths) { - const isLastColumn = column.id === visibleColumns[visibleColumns.length - 1]?.id; - const totalWidth = visibleColumns.reduce( - (sum, { id }) => sum + (columnWidths.get(id) || DEFAULT_COLUMN_WIDTH), - 0 - ); - if (isLastColumn && containerWidthRef.current > totalWidth) { - return { width: 'auto', minWidth: column?.minWidth }; - } else { - return { width: columnWidths.get(column.id), minWidth: column?.minWidth }; + if (resizableColumns && columnWidths) { + const isLastColumn = column.id === visibleColumns[visibleColumns.length - 1]?.id; + const totalWidth = visibleColumns.reduce( + (sum, { id }) => sum + (columnWidths.get(id) || DEFAULT_COLUMN_WIDTH), + 0 + ); + if (isLastColumn && containerWidthRef.current > totalWidth) { + return { width: 'auto', minWidth: column?.minWidth }; + } else { + return { width: columnWidths.get(column.id), minWidth: column?.minWidth }; + } } - } - return { - width: column.width, - minWidth: column.minWidth, - maxWidth: !resizableColumns ? column.maxWidth : undefined, - }; - }; + return { + width: column.width, + minWidth: column.minWidth, + maxWidth: !resizableColumns ? column.maxWidth : undefined, + }; + }, + [columnById, columnWidths, resizableColumns, visibleColumns] + ); // Imperatively sets width style for a cell avoiding React state. // This allows setting the style as soon container's size change is observed. @@ -195,12 +198,25 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - function updateColumn(columnId: PropertyKey, newWidth: number) { - setColumnWidths(columnWidths => updateWidths(columnById, columnWidths ?? new Map(), newWidth, columnId)); - } + const updateColumn = useCallback( + (columnId: PropertyKey, newWidth: number) => { + setColumnWidths(columnWidths => updateWidths(columnById, columnWidths ?? new Map(), newWidth, columnId)); + }, + [columnById] + ); + + const contextValue = useMemo( + () => ({ + getColumnStyles, + columnWidths: columnWidths ?? new Map(), + updateColumn, + setCell, + }), + [getColumnStyles, columnWidths, updateColumn, setCell] + ); return ( - + {children} ); diff --git a/src/top-navigation/parts/overflow-menu/router.tsx b/src/top-navigation/parts/overflow-menu/router.tsx index 10c41fb8d7..0e1db40a9a 100644 --- a/src/top-navigation/parts/overflow-menu/router.tsx +++ b/src/top-navigation/parts/overflow-menu/router.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { createContext, Dispatch, SetStateAction, useContext, useState } from 'react'; +import React, { createContext, Dispatch, SetStateAction, useContext, useMemo, useState } from 'react'; type View = 'utilities' | 'dropdown-menu'; @@ -60,7 +60,8 @@ interface RouterProps { const Router = ({ children }: RouterProps) => { const [state, setState] = useState({ view: 'utilities', data: null }); - return {children}; + const value = useMemo(() => ({ state, setState }), [state]); + return {children}; }; export default Router; From cb094b12865eed339f7e77003e032eec29b4802e Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Wed, 31 Dec 2025 15:37:09 -0500 Subject: [PATCH 3/3] fix: Cache measured column widths to avoid getBoundingClientRect during render --- src/table/use-column-widths.tsx | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/table/use-column-widths.tsx b/src/table/use-column-widths.tsx index 002c512682..6597a10922 100644 --- a/src/table/use-column-widths.tsx +++ b/src/table/use-column-widths.tsx @@ -91,6 +91,8 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain const cellsRef = useRef(new Map()); const stickyCellsRef = useRef(new Map()); + // Cache measured widths from real cells to avoid getBoundingClientRect() during render + const measuredWidthsRef = useRef(new Map()); const getCell = (columnId: PropertyKey): null | HTMLElement => cellsRef.current.get(columnId) ?? null; const setCell = useCallback((sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => { const ref = sticky ? stickyCellsRef : cellsRef; @@ -109,10 +111,10 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain } if (sticky) { + // Use cached measured width to avoid getBoundingClientRect() during render. + // The cache is populated by updateColumnWidths() which runs outside the render path. return { - width: - cellsRef.current.get(column.id)?.getBoundingClientRect().width || - (columnWidths?.get(column.id) ?? column.width), + width: measuredWidthsRef.current.get(column.id) || columnWidths?.get(column.id) || column.width, }; } @@ -140,13 +142,25 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain // Imperatively sets width style for a cell avoiding React state. // This allows setting the style as soon container's size change is observed. const updateColumnWidths = useStableCallback(() => { + // First pass: set widths on real cells for (const { id } of visibleColumns) { const element = cellsRef.current.get(id); if (element) { setElementWidths(element, getColumnStyles(false, id)); } } - // Sticky column widths must be synchronized once all real column widths are assigned. + // Second pass: measure real cell widths and cache them for sticky columns. + // This avoids calling getBoundingClientRect() during render. + for (const { id } of visibleColumns) { + const element = cellsRef.current.get(id); + if (element) { + const width = element.getBoundingClientRect().width; + if (width > 0) { + measuredWidthsRef.current.set(id, width); + } + } + } + // Third pass: set sticky column widths using cached measurements for (const { id } of visibleColumns) { const element = stickyCellsRef.current.get(id); if (element) {