Skip to content

Commit 6c72cce

Browse files
authored
fix: scrolling performance optimizations for table (#2335)
1 parent 828724d commit 6c72cce

File tree

8 files changed

+513
-108
lines changed

8 files changed

+513
-108
lines changed

src/components/PaginatedTable/PaginatedTable.tsx

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22

33
import {usePaginatedTableState} from './PaginatedTableContext';
4-
import {TableChunk} from './TableChunk';
4+
import {TableChunksRenderer} from './TableChunksRenderer';
55
import {TableHead} from './TableHead';
66
import {DEFAULT_TABLE_ROW_HEIGHT} from './constants';
77
import {b} from './shared';
@@ -14,7 +14,7 @@ import type {
1414
RenderEmptyDataMessage,
1515
RenderErrorMessage,
1616
} from './types';
17-
import {useScrollBasedChunks} from './useScrollBasedChunks';
17+
import {calculateElementOffsetTop} from './utils';
1818

1919
import './PaginatedTable.scss';
2020

@@ -62,14 +62,7 @@ export const PaginatedTable = <T, F>({
6262
const {sortParams, foundEntities} = tableState;
6363

6464
const tableRef = React.useRef<HTMLDivElement>(null);
65-
66-
const activeChunks = useScrollBasedChunks({
67-
scrollContainerRef,
68-
tableRef,
69-
totalItems: foundEntities,
70-
rowHeight,
71-
chunkSize,
72-
});
65+
const [tableOffset, setTableOffset] = React.useState(0);
7366

7467
// this prevent situation when filters are new, but active chunks is not yet recalculated (it will be done to the next rendrer, so we bring filters change on the next render too)
7568
const [filters, setFilters] = React.useState(rawFilters);
@@ -78,15 +71,6 @@ export const PaginatedTable = <T, F>({
7871
setFilters(rawFilters);
7972
}, [rawFilters]);
8073

81-
const lastChunkSize = React.useMemo(() => {
82-
// If foundEntities = 0, there will only first chunk
83-
// Display it with 1 row, to display empty data message
84-
if (!foundEntities) {
85-
return 1;
86-
}
87-
return foundEntities % chunkSize || chunkSize;
88-
}, [foundEntities, chunkSize]);
89-
9074
const handleDataFetched = React.useCallback(
9175
(data?: PaginatedTableData<T>) => {
9276
if (data) {
@@ -99,6 +83,25 @@ export const PaginatedTable = <T, F>({
9983
[onDataFetched, setFoundEntities, setIsInitialLoad, setTotalEntities],
10084
);
10185

86+
React.useLayoutEffect(() => {
87+
const scrollContainer = scrollContainerRef.current;
88+
const table = tableRef.current;
89+
if (table && scrollContainer) {
90+
setTableOffset(calculateElementOffsetTop(table, scrollContainer));
91+
}
92+
}, [scrollContainerRef.current, tableRef.current, foundEntities]);
93+
94+
// Set will-change: transform on scroll container if not already set
95+
React.useLayoutEffect(() => {
96+
const scrollContainer = scrollContainerRef.current;
97+
if (scrollContainer) {
98+
const computedStyle = window.getComputedStyle(scrollContainer);
99+
if (computedStyle.willChange !== 'transform') {
100+
scrollContainer.style.willChange = 'transform';
101+
}
102+
}
103+
}, [scrollContainerRef.current]);
104+
102105
// Reset table on initialization and filters change
103106
React.useLayoutEffect(() => {
104107
const defaultTotal = initialEntitiesCount || 0;
@@ -109,33 +112,29 @@ export const PaginatedTable = <T, F>({
109112
setIsInitialLoad(true);
110113
}, [initialEntitiesCount, setTotalEntities, setFoundEntities, setIsInitialLoad]);
111114

112-
const renderChunks = () => {
113-
return activeChunks.map((isActive, index) => (
114-
<TableChunk<T, F>
115-
key={index}
116-
id={index}
117-
calculatedCount={index === activeChunks.length - 1 ? lastChunkSize : chunkSize}
118-
chunkSize={chunkSize}
119-
rowHeight={rowHeight}
120-
columns={columns}
121-
fetchData={fetchData}
122-
filters={filters}
123-
tableName={tableName}
124-
sortParams={sortParams}
125-
getRowClassName={getRowClassName}
126-
renderErrorMessage={renderErrorMessage}
127-
renderEmptyDataMessage={renderEmptyDataMessage}
128-
onDataFetched={handleDataFetched}
129-
isActive={isActive}
130-
keepCache={keepCache}
131-
/>
132-
));
133-
};
134-
135115
const renderTable = () => (
136116
<table className={b('table')}>
137117
<TableHead columns={columns} onSort={setSortParams} onColumnsResize={onColumnsResize} />
138-
{renderChunks()}
118+
<tbody>
119+
<TableChunksRenderer
120+
scrollContainerRef={scrollContainerRef}
121+
tableRef={tableRef}
122+
foundEntities={foundEntities}
123+
tableOffset={tableOffset}
124+
chunkSize={chunkSize}
125+
rowHeight={rowHeight}
126+
columns={columns}
127+
fetchData={fetchData}
128+
filters={filters}
129+
tableName={tableName}
130+
sortParams={sortParams}
131+
getRowClassName={getRowClassName}
132+
renderErrorMessage={renderErrorMessage}
133+
renderEmptyDataMessage={renderEmptyDataMessage}
134+
onDataFetched={handleDataFetched}
135+
keepCache={keepCache}
136+
/>
137+
</tbody>
139138
</table>
140139
);
141140

src/components/PaginatedTable/TableChunk.tsx

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ interface TableChunkProps<T, F> {
2929
columns: Column<T>[];
3030
filters?: F;
3131
sortParams?: SortParams;
32-
isActive: boolean;
32+
shouldFetch: boolean;
33+
shouldRender: boolean;
3334
tableName: string;
3435

3536
fetchData: FetchData<T, F>;
@@ -56,7 +57,8 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
5657
renderErrorMessage,
5758
renderEmptyDataMessage,
5859
onDataFetched,
59-
isActive,
60+
shouldFetch,
61+
shouldRender,
6062
keepCache,
6163
}: TableChunkProps<T, F>) {
6264
const [isTimeoutActive, setIsTimeoutActive] = React.useState(true);
@@ -75,7 +77,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
7577
};
7678

7779
tableDataApi.useFetchTableChunkQuery(queryParams, {
78-
skip: isTimeoutActive || !isActive,
80+
skip: isTimeoutActive || !shouldFetch,
7981
pollingInterval: autoRefreshInterval,
8082
refetchOnMountOrArgChange: !keepCache,
8183
});
@@ -85,7 +87,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
8587
React.useEffect(() => {
8688
let timeout = 0;
8789

88-
if (isActive && isTimeoutActive) {
90+
if (shouldFetch && isTimeoutActive) {
8991
timeout = window.setTimeout(() => {
9092
setIsTimeoutActive(false);
9193
}, DEBOUNCE_TIMEOUT);
@@ -94,31 +96,27 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
9496
return () => {
9597
window.clearTimeout(timeout);
9698
};
97-
}, [isActive, isTimeoutActive]);
99+
}, [shouldFetch, isTimeoutActive]);
98100

99101
React.useEffect(() => {
100-
if (currentData && isActive) {
102+
if (currentData) {
101103
onDataFetched({
102104
...currentData,
103105
data: currentData.data as T[],
104106
found: currentData.found || 0,
105107
total: currentData.total || 0,
106108
});
107109
}
108-
}, [currentData, isActive, onDataFetched]);
110+
}, [currentData, onDataFetched]);
109111

110112
const dataLength = currentData?.data?.length || calculatedCount;
111113

112114
const renderContent = () => {
113-
if (!isActive) {
114-
return null;
115-
}
116-
117115
if (!currentData) {
118116
if (error) {
119117
const errorData = error as IResponseError;
120118
return (
121-
<EmptyTableRow columns={columns}>
119+
<EmptyTableRow columns={columns} height={dataLength * rowHeight}>
122120
{renderErrorMessage ? (
123121
renderErrorMessage(errorData)
124122
) : (
@@ -136,7 +134,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
136134
// Data is loaded, but there are no entities in the chunk
137135
if (!currentData.data?.length) {
138136
return (
139-
<EmptyTableRow columns={columns}>
137+
<EmptyTableRow columns={columns} height={dataLength * rowHeight}>
140138
{renderEmptyDataMessage ? renderEmptyDataMessage() : i18n('empty')}
141139
</EmptyTableRow>
142140
);
@@ -153,18 +151,5 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
153151
));
154152
};
155153

156-
return (
157-
<tbody
158-
id={id.toString()}
159-
style={{
160-
height: `${dataLength * rowHeight}px`,
161-
// Default display: table-row-group doesn't work in Safari and breaks the table
162-
// display: block works in Safari, but disconnects thead and tbody cell grids
163-
// Hack to make it work in all cases
164-
display: isActive ? 'table-row-group' : 'block',
165-
}}
166-
>
167-
{renderContent()}
168-
</tbody>
169-
);
154+
return shouldRender ? renderContent() : null;
170155
});

0 commit comments

Comments
 (0)