fix: Improve Kubernetes dashboard performance (#1333)

These are the minimal set of changes needed to improve the kubernetes dashboard with 100k+ pods.

**Changes:**
* Fixed a performance issue in ChartUtils that caused computation to be O(n^2). This caused charts to slow down rendering to a crawl and freeze the page. It's not as noticeable with a smaller data set. This was the main issue.
* Limited the number of items returned in the nodes, namespaces, and pods tables to 10k. This was the second biggest issue.
* Introduced a virtualized table to each of the tables to speed up rendering. This was the third biggest issue.
* Increased the amount of unique items returned from the metadata query so that users can filter for the items they need (UX improvement)

**Future changes that will improve the experience even more:**
1) Fetch 10k, but add pagination (UX)
2) Improve query for fetching tabular data. It's timeseries, but realistically, we can make a smarter more performant query
3) To fill out the data in the tables (cpu, memory, uptime, etc...) we make separate queries and combine them on the server side. We could make this one large query (we have an existing ticket in the backlog for it).
4) Chart rendering is very computational intensive. It would be a better user experience to load these after the table loads.

**Outstanding (existing) bugs that exist that I will fix in follow-up tickets:**
1) The namespaces query uses the wrong time window. It does not respect the global time picker date range.
2) Sorting of the table columns is broken.

Ref: HDX-2370

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Tom Alexander 2025-11-07 13:28:45 -05:00 committed by GitHub
parent 2743d85b37
commit 892e43f889
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 453 additions and 220 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: Improve loading of kubernetes dashboard

View file

@ -600,7 +600,11 @@ export function formatResponseForTimeChart({
const ts = date.getTime() / 1000;
for (const valueColumn of valueColumns) {
const tsBucket = tsBucketMap.get(ts) ?? {};
let tsBucket = tsBucketMap.get(ts);
if (tsBucket == null) {
tsBucket = { [timestampColumn.name]: ts };
tsBucketMap.set(ts, tsBucket);
}
const keyName = [
// Simplify the display name if there's only one series and a group by
@ -614,11 +618,8 @@ export function formatResponseForTimeChart({
const value =
typeof rawValue === 'number' ? rawValue : Number.parseFloat(rawValue);
tsBucketMap.set(ts, {
...tsBucket,
[timestampColumn.name]: ts,
[keyName]: value,
});
// Mutate the existing bucket object to avoid repeated large object copies
tsBucket[keyName] = value;
let color: string | undefined = undefined;
if (

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import cx from 'classnames';
@ -8,6 +8,7 @@ import { useQueryState } from 'nuqs';
import { useForm } from 'react-hook-form';
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
import {
Alert,
Badge,
Box,
Button,
@ -15,7 +16,6 @@ import {
Flex,
Grid,
Group,
ScrollArea,
SegmentedControl,
Skeleton,
Table,
@ -24,8 +24,10 @@ import {
Tooltip,
} from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { useVirtualizer } from '@tanstack/react-virtual';
import { TimePicker } from '@/components/TimePicker';
import { useVirtualList } from '@/hooks/useVirtualList';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import { DBTimeChart } from './components/DBTimeChart';
@ -51,8 +53,6 @@ import { parseTimeQuery, useTimeQuery } from './timeQuery';
import { KubePhase } from './types';
import { formatNumber, formatUptime } from './utils';
const makeId = () => Math.floor(100000000 * Math.random()).toString(36);
const getKubePhaseNumber = (phase: string) => {
switch (phase) {
case 'running':
@ -92,6 +92,8 @@ const Th = React.memo<{
);
});
const TABLE_FETCH_LIMIT = 10000;
type InfraPodsStatusTableColumn =
| 'restarts'
| 'uptime'
@ -147,8 +149,8 @@ export const InfraPodsStatusTable = ({
});
const groupBy = ['k8s.pod.name', 'k8s.namespace.name', 'k8s.node.name'];
const { data, isError, isLoading } = useQueriedChartConfig(
convertV1ChartConfigToV2(
const { data, isError, isLoading } = useQueriedChartConfig({
...convertV1ChartConfigToV2(
{
series: [
{
@ -230,7 +232,8 @@ export const InfraPodsStatusTable = ({
metric: metricSource,
},
),
);
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
});
// TODO: Use useTable
const podsList = React.useMemo(() => {
@ -238,31 +241,35 @@ export const InfraPodsStatusTable = ({
return [];
}
return data.data
.map((row: any) => {
return {
id: makeId(),
name: row["arrayElement(ResourceAttributes, 'k8s.pod.name')"],
namespace:
row["arrayElement(ResourceAttributes, 'k8s.namespace.name')"],
node: row["arrayElement(ResourceAttributes, 'k8s.node.name')"],
restarts: row['last_value(k8s.container.restarts)'],
uptime: row['undefined(k8s.pod.uptime)'],
cpuAvg: row['avg(k8s.pod.cpu.utilization)'],
cpuLimitUtilization: row['avg(k8s.pod.cpu_limit_utilization)'],
memAvg: row['avg(k8s.pod.memory.usage)'],
memLimitUtilization: row['avg(k8s.pod.memory_limit_utilization)'],
phase: row['last_value(k8s.pod.phase)'],
};
})
.filter(pod => {
if (phaseFilter === 'all') {
return true;
}
return pod.phase === getKubePhaseNumber(phaseFilter);
});
// Filter first to reduce the number of objects we create
const phaseFilteredData = data.data.filter((row: any) => {
if (phaseFilter === 'all') {
return true;
}
return (
row['last_value(k8s.pod.phase)'] === getKubePhaseNumber(phaseFilter)
);
});
// Transform only the filtered data
return phaseFilteredData.map((row: any, index: number) => ({
id: `pod-${index}`, // Use index-based ID instead of random makeId()
name: row["arrayElement(ResourceAttributes, 'k8s.pod.name')"],
namespace: row["arrayElement(ResourceAttributes, 'k8s.namespace.name')"],
node: row["arrayElement(ResourceAttributes, 'k8s.node.name')"],
restarts: row['last_value(k8s.container.restarts)'],
uptime: row['undefined(k8s.pod.uptime)'],
cpuAvg: row['avg(k8s.pod.cpu.utilization)'],
cpuLimitUtilization: row['avg(k8s.pod.cpu_limit_utilization)'],
memAvg: row['avg(k8s.pod.memory.usage)'],
memLimitUtilization: row['avg(k8s.pod.memory_limit_utilization)'],
phase: row['last_value(k8s.pod.phase)'],
}));
}, [data, phaseFilter]);
// Check if we're hitting the fetch limit (indicating there might be more data)
const isAtFetchLimit = data?.data && data.data.length >= TABLE_FETCH_LIMIT;
const getLink = React.useCallback((podName: string) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set('podName', `${podName}`);
@ -279,6 +286,32 @@ export const InfraPodsStatusTable = ({
sort: sortState.column === column ? sortState.order : null,
});
const tableContainerRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: podsList.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: useCallback(() => 40, []),
overscan: 10,
});
const virtualItems = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
const [paddingTop, paddingBottom] = useMemo(
() =>
virtualItems.length > 0
? [
Math.max(
0,
virtualItems[0].start - rowVirtualizer.options.scrollMargin,
),
Math.max(0, totalSize - virtualItems[virtualItems.length - 1].end),
]
: [0, 0],
[virtualItems, rowVirtualizer.options.scrollMargin, totalSize],
);
return (
<Card p="md" data-testid="k8s-pods-table">
<Card.Section p="md" py="xs" withBorder>
@ -298,10 +331,20 @@ export const InfraPodsStatusTable = ({
/>
</Group>
</Card.Section>
{isAtFetchLimit && !isLoading && !isError && podsList.length > 0 && (
<Card.Section px="md" py="xs">
<Alert variant="light" color="blue">
Showing first {TABLE_FETCH_LIMIT.toLocaleString()} pods. Use the
filters above to narrow your search.
</Alert>
</Card.Section>
)}
<Card.Section>
<ScrollArea
viewportProps={{
style: { maxHeight: 300 },
<div
ref={tableContainerRef}
style={{
height: '300px',
overflow: 'auto',
}}
>
{isLoading ? (
@ -339,88 +382,111 @@ export const InfraPodsStatusTable = ({
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{podsList.map(pod => (
<Link key={pod.id} href={getLink(pod.name)} legacyBehavior>
<Table.Tr className="cursor-pointer">
<Table.Td>{pod.name}</Table.Td>
<Table.Td
data-testid={`k8s-pods-table-namespace-${pod.id}`}
{paddingTop > 0 && (
<Table.Tr>
<Table.Td
colSpan={8}
style={{ height: `${paddingTop}px` }}
/>
</Table.Tr>
)}
{virtualItems.map(virtualRow => {
const pod = podsList[virtualRow.index];
return (
<Link key={pod.id} href={getLink(pod.name)} legacyBehavior>
<Table.Tr
className="cursor-pointer"
ref={rowVirtualizer.measureElement}
data-index={virtualRow.index}
>
{pod.namespace}
</Table.Td>
<Table.Td>{pod.node}</Table.Td>
<Table.Td>
<FormatPodStatus status={pod.phase} />
</Table.Td>
<Table.Td>
<Tooltip
color="gray"
label={
formatNumber(
pod.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
) + ' avg'
}
<Table.Td>{pod.name}</Table.Td>
<Table.Td
data-testid={`k8s-pods-table-namespace-${pod.id}`}
>
<Text
span
c={pod.cpuLimitUtilization ? undefined : 'gray.7'}
{pod.namespace}
</Table.Td>
<Table.Td>{pod.node}</Table.Td>
<Table.Td>
<FormatPodStatus status={pod.phase} />
</Table.Td>
<Table.Td>
<Tooltip
color="gray"
label={
formatNumber(
pod.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
) + ' avg'
}
>
{pod.cpuLimitUtilization
? formatNumber(
pod.cpuLimitUtilization,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)
: '-'}
</Text>
</Tooltip>
</Table.Td>
<Table.Td>
<Tooltip
color="gray"
label={
formatNumber(pod.memAvg, K8S_MEM_NUMBER_FORMAT) +
' avg'
}
>
<Text
span
c={pod.memLimitUtilization ? undefined : 'gray.7'}
<Text
span
c={pod.cpuLimitUtilization ? undefined : 'gray.7'}
>
{pod.cpuLimitUtilization
? formatNumber(
pod.cpuLimitUtilization,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)
: '-'}
</Text>
</Tooltip>
</Table.Td>
<Table.Td>
<Tooltip
color="gray"
label={
formatNumber(pod.memAvg, K8S_MEM_NUMBER_FORMAT) +
' avg'
}
>
{pod.memLimitUtilization
? formatNumber(
pod.memLimitUtilization,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)
: '-'}
<Text
span
c={pod.memLimitUtilization ? undefined : 'gray.7'}
>
{pod.memLimitUtilization
? formatNumber(
pod.memLimitUtilization,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)
: '-'}
</Text>
</Tooltip>
</Table.Td>
<Table.Td>
<Text span c={pod.uptime ? undefined : 'gray.7'}>
{pod.uptime ? formatUptime(pod.uptime) : ''}
</Text>
</Tooltip>
</Table.Td>
<Table.Td>
<Text span c={pod.uptime ? undefined : 'gray.7'}>
{pod.uptime ? formatUptime(pod.uptime) : ''}
</Text>
</Table.Td>
<Table.Td>
<Text
color={
pod.restarts >= 10
? 'red.6'
: pod.restarts >= 5
? 'yellow.3'
: 'grey.7'
}
>
{pod.restarts}
</Text>
</Table.Td>
</Table.Tr>
</Link>
))}
</Table.Td>
<Table.Td>
<Text
color={
pod.restarts >= 10
? 'red.6'
: pod.restarts >= 5
? 'yellow.3'
: 'grey.7'
}
>
{pod.restarts}
</Text>
</Table.Td>
</Table.Tr>
</Link>
);
})}
{paddingBottom > 0 && (
<Table.Tr>
<Table.Td
colSpan={8}
style={{ height: `${paddingBottom}px` }}
/>
</Table.Tr>
)}
</Table.Tbody>
</Table>
)}
</ScrollArea>
</div>
</Card.Section>
</Card>
);
@ -437,8 +503,8 @@ const NodesTable = ({
}) => {
const groupBy = ['k8s.node.name'];
const { data, isError, isLoading } = useQueriedChartConfig(
convertV1ChartConfigToV2(
const { data, isError, isLoading } = useQueriedChartConfig({
...convertV1ChartConfigToV2(
{
series: [
{
@ -481,7 +547,8 @@ const NodesTable = ({
metric: metricSource,
},
),
);
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
});
const getLink = React.useCallback((nodeName: string) => {
const searchParams = new URLSearchParams(window.location.search);
@ -505,15 +572,25 @@ const NodesTable = ({
});
}, [data]);
const {
containerRef: nodesContainerRef,
rowVirtualizer: nodesRowVirtualizer,
virtualItems: nodeVirtualItems,
paddingTop: nodesPaddingTop,
paddingBottom: nodesPaddingBottom,
} = useVirtualList(nodesList.length, 40, 10);
return (
<Card p="md" data-testid="k8s-nodes-table">
<Card.Section p="md" py="xs" withBorder>
Nodes
</Card.Section>
<Card.Section>
<ScrollArea
viewportProps={{
style: { maxHeight: 300 },
<div
ref={nodesContainerRef}
style={{
height: '300px',
overflow: 'auto',
}}
>
{isLoading ? (
@ -537,58 +614,80 @@ const NodesTable = ({
<Table.Th style={{ width: 130 }}>Uptime</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{nodesList.map(node => (
<Link
key={node.name}
href={getLink(node.name)}
legacyBehavior
>
<Table.Tr className="cursor-pointer">
<Table.Td>{node.name || 'N/A'}</Table.Td>
<Table.Td>
{node.ready === 1 ? (
<Badge
variant="light"
color="green"
fw="normal"
tt="none"
size="md"
>
Ready
</Badge>
) : (
<Badge
variant="light"
color="red"
fw="normal"
tt="none"
size="md"
>
Not Ready
</Badge>
)}
</Table.Td>
<Table.Td>
{formatNumber(
node.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)}
</Table.Td>
<Table.Td>
{formatNumber(node.memAvg, K8S_MEM_NUMBER_FORMAT)}
</Table.Td>
<Table.Td>
{node.uptime ? formatUptime(node.uptime) : ''}
</Table.Td>
</Table.Tr>
</Link>
))}
{nodesPaddingTop > 0 && (
<Table.Tr>
<Table.Td
colSpan={5}
style={{ height: `${nodesPaddingTop}px` }}
/>
</Table.Tr>
)}
{nodeVirtualItems.map(virtualRow => {
const node = nodesList[virtualRow.index];
return (
<Link
key={node.name}
href={getLink(node.name)}
legacyBehavior
>
<Table.Tr
className="cursor-pointer"
ref={nodesRowVirtualizer.measureElement}
data-index={virtualRow.index}
>
<Table.Td>{node.name || 'N/A'}</Table.Td>
<Table.Td>
{node.ready === 1 ? (
<Badge
variant="light"
color="green"
fw="normal"
tt="none"
size="md"
>
Ready
</Badge>
) : (
<Badge
variant="light"
color="red"
fw="normal"
tt="none"
size="md"
>
Not Ready
</Badge>
)}
</Table.Td>
<Table.Td>
{formatNumber(
node.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)}
</Table.Td>
<Table.Td>
{formatNumber(node.memAvg, K8S_MEM_NUMBER_FORMAT)}
</Table.Td>
<Table.Td>
{node.uptime ? formatUptime(node.uptime) : ''}
</Table.Td>
</Table.Tr>
</Link>
);
})}
{nodesPaddingBottom > 0 && (
<Table.Tr>
<Table.Td
colSpan={5}
style={{ height: `${nodesPaddingBottom}px` }}
/>
</Table.Tr>
)}
</Table.Tbody>
</Table>
)}
</ScrollArea>
</div>
</Card.Section>
</Card>
);
@ -605,8 +704,8 @@ const NamespacesTable = ({
}) => {
const groupBy = ['k8s.namespace.name'];
const { data, isError, isLoading } = useQueriedChartConfig(
convertV1ChartConfigToV2(
const { data, isError, isLoading } = useQueriedChartConfig({
...convertV1ChartConfigToV2(
{
series: [
{
@ -646,7 +745,8 @@ const NamespacesTable = ({
metric: metricSource,
},
),
);
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
});
const namespacesList = React.useMemo(() => {
if (!data) {
@ -663,6 +763,14 @@ const NamespacesTable = ({
});
}, [data]);
const {
containerRef: namespacesContainerRef,
rowVirtualizer: namespacesRowVirtualizer,
virtualItems: namespaceVirtualItems,
paddingTop: namespacesPaddingTop,
paddingBottom: namespacesPaddingBottom,
} = useVirtualList(namespacesList.length, 40, 10);
const getLink = React.useCallback((namespaceName: string) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set('namespaceName', `${namespaceName}`);
@ -675,9 +783,11 @@ const NamespacesTable = ({
Namespaces
</Card.Section>
<Card.Section>
<ScrollArea
viewportProps={{
style: { maxHeight: 300 },
<div
ref={namespacesContainerRef}
style={{
height: '300px',
overflow: 'auto',
}}
>
{isLoading ? (
@ -701,53 +811,79 @@ const NamespacesTable = ({
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{namespacesList.map(namespace => (
<Link
key={namespace.name}
href={getLink(namespace.name)}
legacyBehavior
>
<Table.Tr className="cursor-pointer">
<Table.Td>{namespace.name || 'N/A'}</Table.Td>
<Table.Td>
{namespace.phase === 1 ? (
<Badge
variant="light"
color="green"
fw="normal"
tt="none"
size="md"
>
Ready
</Badge>
) : (
<Badge
variant="light"
color="red"
fw="normal"
tt="none"
size="md"
>
Terminating
</Badge>
)}
</Table.Td>
<Table.Td>
{formatNumber(
namespace.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)}
</Table.Td>
<Table.Td>
{formatNumber(namespace.memAvg, K8S_MEM_NUMBER_FORMAT)}
</Table.Td>
</Table.Tr>
</Link>
))}
{namespacesPaddingTop > 0 && (
<Table.Tr>
<Table.Td
colSpan={4}
style={{ height: `${namespacesPaddingTop}px` }}
/>
</Table.Tr>
)}
{namespaceVirtualItems.map(virtualRow => {
const namespace = namespacesList[virtualRow.index];
return (
<Link
key={namespace.name}
href={getLink(namespace.name)}
legacyBehavior
>
<Table.Tr
className="cursor-pointer"
ref={namespacesRowVirtualizer.measureElement}
data-index={virtualRow.index}
>
<Table.Td>{namespace.name || 'N/A'}</Table.Td>
<Table.Td>
{namespace.phase === 1 ? (
<Badge
variant="light"
color="green"
fw="normal"
tt="none"
size="md"
>
Ready
</Badge>
) : (
<Badge
variant="light"
color="red"
fw="normal"
tt="none"
size="md"
>
Terminating
</Badge>
)}
</Table.Td>
<Table.Td>
{formatNumber(
namespace.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)}
</Table.Td>
<Table.Td>
{formatNumber(
namespace.memAvg,
K8S_MEM_NUMBER_FORMAT,
)}
</Table.Td>
</Table.Tr>
</Link>
);
})}
{namespacesPaddingBottom > 0 && (
<Table.Tr>
<Table.Td
colSpan={4}
style={{ height: `${namespacesPaddingBottom}px` }}
/>
</Table.Tr>
)}
</Table.Tbody>
</Table>
)}
</ScrollArea>
</div>
</Card.Section>
</Card>
);

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import {
@ -39,12 +39,22 @@ const FilterSelect: React.FC<FilterSelectProps> = ({
const { data, isLoading } = useGetKeyValues({
chartConfig,
keys: [`${metricSource.resourceAttributesExpression}['${fieldName}']`],
disableRowLimit: true,
limit: 1000000,
});
const options = useMemo(
() =>
data?.[0]?.value
.map(value => ({ value, label: value }))
.sort((a, b) => a.value.localeCompare(b.value)) || [], // Sort alphabetically for better search results
[data],
);
return (
<Select
placeholder={placeholder + (isLoading ? ' (loading...)' : '')}
data={data?.[0]?.value.map(value => ({ value, label: value })) || []}
data={options}
value={value}
onChange={onChange}
searchable
@ -55,7 +65,7 @@ const FilterSelect: React.FC<FilterSelectProps> = ({
disabled={isLoading}
variant="filled"
w={200}
limit={20}
limit={100} // Show up to 100 search results
data-testid={dataTestId}
/>
);

View file

@ -0,0 +1,81 @@
'use client';
import { useCallback, useMemo, useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
/**
* A custom hook for virtualizing large lists to improve rendering performance.
* Uses @tanstack/react-virtual under the hood to only render visible items.
*
* @param count - Total number of items in the list
* @param estimate - Estimated height of each row in pixels
* @param overscan - Number of items to render outside the visible area (default: 10)
*
* @returns An object containing:
* - containerRef: Ref to attach to the scrollable container element
* - rowVirtualizer: The virtualizer instance for advanced usage
* - virtualItems: Array of currently visible items with their indices and sizes
* - paddingTop: Top padding value to maintain scroll position
* - paddingBottom: Bottom padding value to maintain scroll position
*
* @example
* ```tsx
* const MyList = ({ items }) => {
* const { containerRef, virtualItems, paddingTop, paddingBottom } = useVirtualList(
* items.length,
* 40, // 40px estimated row height
* 10 // render 10 items outside viewport
* );
*
* return (
* <div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
* {paddingTop > 0 && <div style={{ height: paddingTop }} />}
* {virtualItems.map(virtualRow => (
* <div key={virtualRow.index}>
* {items[virtualRow.index].name}
* </div>
* ))}
* {paddingBottom > 0 && <div style={{ height: paddingBottom }} />}
* </div>
* );
* };
* ```
*/
export const useVirtualList = (
count: number,
estimate: number,
overscan: number = 10,
) => {
const containerRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count,
getScrollElement: () => containerRef.current,
estimateSize: useCallback(() => estimate, [estimate]),
overscan,
});
const virtualItems = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
const [paddingTop, paddingBottom] = useMemo(
() =>
virtualItems.length > 0
? [
Math.max(
0,
virtualItems[0].start - rowVirtualizer.options.scrollMargin,
),
Math.max(0, totalSize - virtualItems[virtualItems.length - 1].end),
]
: [0, 0],
[virtualItems, rowVirtualizer.options.scrollMargin, totalSize],
);
return {
containerRef,
rowVirtualizer,
virtualItems,
paddingTop,
paddingBottom,
};
};