Skip to content

feat: find out performance issue with big tables of nodes #2332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
68 changes: 33 additions & 35 deletions src/components/PaginatedTable/PaginatedTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';

import {usePaginatedTableState} from './PaginatedTableContext';
import {TableChunk} from './TableChunk';
import {TableHead} from './TableHead';
import {DEFAULT_TABLE_ROW_HEIGHT} from './constants';
import {b} from './shared';
Expand All @@ -15,6 +14,7 @@ import type {
RenderErrorMessage,
} from './types';
import {useScrollBasedChunks} from './useScrollBasedChunks';
import {useVirtualizedTbodies} from './useVirtualizedTbodies';

import './PaginatedTable.scss';

Expand All @@ -36,7 +36,7 @@ export interface PaginatedTableProps<T, F> {
keepCache?: boolean;
}

const DEFAULT_PAGINATION_LIMIT = 20;
const DEFAULT_PAGINATION_LIMIT = 50;

export const PaginatedTable = <T, F>({
limit: chunkSize = DEFAULT_PAGINATION_LIMIT,
Expand All @@ -63,7 +63,7 @@ export const PaginatedTable = <T, F>({

const tableRef = React.useRef<HTMLDivElement>(null);

const activeChunks = useScrollBasedChunks({
const {visibleRowRange, totalItems} = useScrollBasedChunks({
scrollContainerRef,
tableRef,
totalItems: foundEntities,
Expand All @@ -78,15 +78,6 @@ export const PaginatedTable = <T, F>({
setFilters(rawFilters);
}, [rawFilters]);

const lastChunkSize = React.useMemo(() => {
// If foundEntities = 0, there will only first chunk
// Display it with 1 row, to display empty data message
if (!foundEntities) {
return 1;
}
return foundEntities % chunkSize || chunkSize;
}, [foundEntities, chunkSize]);

const handleDataFetched = React.useCallback(
(data?: PaginatedTableData<T>) => {
if (data) {
Expand All @@ -99,6 +90,17 @@ export const PaginatedTable = <T, F>({
[onDataFetched, setFoundEntities, setIsInitialLoad, setTotalEntities],
);

// Set will-change: transform on scroll container if not already set
React.useLayoutEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
const computedStyle = window.getComputedStyle(scrollContainer);
if (computedStyle.willChange !== 'transform') {
scrollContainer.style.willChange = 'transform';
}
}
}, [scrollContainerRef.current]);

// Reset table on initialization and filters change
React.useLayoutEffect(() => {
const defaultTotal = initialEntitiesCount || 0;
Expand All @@ -109,33 +111,29 @@ export const PaginatedTable = <T, F>({
setIsInitialLoad(true);
}, [initialEntitiesCount, setTotalEntities, setFoundEntities, setIsInitialLoad]);

const renderChunks = () => {
return activeChunks.map((isActive, index) => (
<TableChunk<T, F>
key={index}
id={index}
calculatedCount={index === activeChunks.length - 1 ? lastChunkSize : chunkSize}
chunkSize={chunkSize}
rowHeight={rowHeight}
columns={columns}
fetchData={fetchData}
filters={filters}
tableName={tableName}
sortParams={sortParams}
getRowClassName={getRowClassName}
renderErrorMessage={renderErrorMessage}
renderEmptyDataMessage={renderEmptyDataMessage}
onDataFetched={handleDataFetched}
isActive={isActive}
keepCache={keepCache}
/>
));
};
const {renderChunks} = useVirtualizedTbodies({
visibleRowRange,
totalItems,
chunkSize,
rowHeight,
columns,
fetchData,
filters,
tableName,
sortParams,
getRowClassName,
renderErrorMessage,
renderEmptyDataMessage,
onDataFetched: handleDataFetched,
keepCache,
});

const renderTable = () => (
<table className={b('table')}>
<TableHead columns={columns} onSort={setSortParams} onColumnsResize={onColumnsResize} />
{renderChunks()}
<div className={b('table-body')} style={{height: totalItems * rowHeight}}>
{renderChunks()}
</div>
</table>
);

Expand Down
93 changes: 48 additions & 45 deletions src/components/PaginatedTable/TableChunk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ interface TableChunkProps<T, F> {
columns: Column<T>[];
filters?: F;
sortParams?: SortParams;
isActive: boolean;
tableName: string;
startRow: number;
endRow: number;

fetchData: FetchData<T, F>;
getRowClassName?: GetRowClassName<T>;
Expand All @@ -56,8 +57,9 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
renderErrorMessage,
renderEmptyDataMessage,
onDataFetched,
isActive,
keepCache,
startRow,
endRow,
}: TableChunkProps<T, F>) {
const [isTimeoutActive, setIsTimeoutActive] = React.useState(true);
const [autoRefreshInterval] = useAutoRefreshInterval();
Expand All @@ -75,7 +77,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
};

tableDataApi.useFetchTableChunkQuery(queryParams, {
skip: isTimeoutActive || !isActive,
skip: isTimeoutActive,
pollingInterval: autoRefreshInterval,
refetchOnMountOrArgChange: !keepCache,
});
Expand All @@ -85,7 +87,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
React.useEffect(() => {
let timeout = 0;

if (isActive && isTimeoutActive) {
if (isTimeoutActive) {
timeout = window.setTimeout(() => {
setIsTimeoutActive(false);
}, DEBOUNCE_TIMEOUT);
Expand All @@ -94,77 +96,78 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
return () => {
window.clearTimeout(timeout);
};
}, [isActive, isTimeoutActive]);
}, [isTimeoutActive]);

React.useEffect(() => {
if (currentData && isActive) {
if (currentData) {
onDataFetched({
...currentData,
data: currentData.data as T[],
found: currentData.found || 0,
total: currentData.total || 0,
});
}
}, [currentData, isActive, onDataFetched]);
}, [currentData, onDataFetched]);

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

const renderContent = () => {
if (!isActive) {
return null;
}

if (!currentData) {
if (error) {
const errorData = error as IResponseError;
return (
<EmptyTableRow columns={columns}>
return [
<EmptyTableRow key="empty" columns={columns}>
{renderErrorMessage ? (
renderErrorMessage(errorData)
) : (
<ResponseError error={errorData} />
)}
</EmptyTableRow>
);
</EmptyTableRow>,
];
} else {
return getArray(dataLength).map((value) => (
<LoadingTableRow key={value} columns={columns} height={rowHeight} />
));
return getArray(dataLength)
.map((value, index) => {
const globalRowIndex = id * chunkSize + index;

if (globalRowIndex < startRow || globalRowIndex > endRow) {
return null;
}

return <LoadingTableRow key={value} columns={columns} height={rowHeight} />;
})
.filter(Boolean);
}
}

// Data is loaded, but there are no entities in the chunk
if (!currentData.data?.length) {
return (
<EmptyTableRow columns={columns}>
return [
<EmptyTableRow key="empty" columns={columns}>
{renderEmptyDataMessage ? renderEmptyDataMessage() : i18n('empty')}
</EmptyTableRow>
);
</EmptyTableRow>,
];
}

return currentData.data.map((rowData, index) => (
<TableRow
key={index}
row={rowData as T}
columns={columns}
height={rowHeight}
getRowClassName={getRowClassName}
/>
));
return currentData.data
.map((rowData, index) => {
const globalRowIndex = id * chunkSize + index;

if (globalRowIndex < startRow || globalRowIndex > endRow) {
return null;
}

return (
<TableRow
key={index}
row={rowData as T}
columns={columns}
height={rowHeight}
getRowClassName={getRowClassName}
/>
);
})
.filter(Boolean);
};

return (
<tbody
id={id.toString()}
style={{
height: `${dataLength * rowHeight}px`,
// Default display: table-row-group doesn't work in Safari and breaks the table
// display: block works in Safari, but disconnects thead and tbody cell grids
// Hack to make it work in all cases
display: isActive ? 'table-row-group' : 'block',
}}
>
{renderContent()}
</tbody>
);
return renderContent();
});
54 changes: 33 additions & 21 deletions src/components/PaginatedTable/useScrollBasedChunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface UseScrollBasedChunksProps {
overscanCount?: number;
}

const DEFAULT_OVERSCAN_COUNT = 1;
const DEFAULT_OVERSCAN_COUNT = 15;

export const useScrollBasedChunks = ({
scrollContainerRef,
Expand All @@ -20,15 +20,18 @@ export const useScrollBasedChunks = ({
rowHeight,
chunkSize,
overscanCount = DEFAULT_OVERSCAN_COUNT,
}: UseScrollBasedChunksProps): boolean[] => {
}: UseScrollBasedChunksProps): {
visibleRowRange: {start: number; end: number};
totalItems: number;
} => {
const chunksCount = React.useMemo(
() => Math.ceil(totalItems / chunkSize),
[chunkSize, totalItems],
);

const [startChunk, setStartChunk] = React.useState(0);
const [endChunk, setEndChunk] = React.useState(
Math.min(overscanCount, Math.max(chunksCount - 1, 0)),
const [startRow, setStartRow] = React.useState(0);
const [endRow, setEndRow] = React.useState(
Math.min(overscanCount, Math.max(totalItems - 1, 0)),
);

const calculateVisibleRange = React.useCallback(() => {
Expand All @@ -43,19 +46,30 @@ export const useScrollBasedChunks = ({
const visibleStart = Math.max(containerScroll - tableOffset, 0);
const visibleEnd = visibleStart + container.clientHeight;

const start = Math.max(Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, 0);
const end = Math.min(
Math.floor(visibleEnd / rowHeight / chunkSize) + overscanCount,
Math.max(chunksCount - 1, 0),
);
return {start, end};
}, [scrollContainerRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);
// Calculate row range first
const rowStart = Math.max(Math.floor(visibleStart / rowHeight) - overscanCount, 0);
const rowEnd = Math.min(Math.floor(visibleEnd / rowHeight) + overscanCount, totalItems - 1);

// Calculate chunk range from row range
const start = Math.max(Math.floor(rowStart / chunkSize), 0);
const end = Math.min(Math.floor(rowEnd / chunkSize), Math.max(chunksCount - 1, 0));

return {start, end, rowStart, rowEnd};
}, [
scrollContainerRef,
tableRef,
rowHeight,
chunkSize,
overscanCount,
chunksCount,
totalItems,
]);

const updateVisibleChunks = React.useCallback(() => {
const newRange = calculateVisibleRange();
if (newRange) {
setStartChunk(newRange.start);
setEndChunk(newRange.end);
setStartRow(newRange.rowStart);
setEndRow(newRange.rowEnd);
}
}, [calculateVisibleRange]);

Expand Down Expand Up @@ -94,11 +108,9 @@ export const useScrollBasedChunks = ({
}, [handleScroll, scrollContainerRef]);

return React.useMemo(() => {
// boolean array that represents active chunks
const activeChunks = Array(chunksCount).fill(false);
for (let i = startChunk; i <= endChunk; i++) {
activeChunks[i] = true;
}
return activeChunks;
}, [chunksCount, startChunk, endChunk]);
return {
visibleRowRange: {start: startRow, end: endRow},
totalItems,
};
}, [startRow, endRow, totalItems]);
};
Loading
Loading