Skip virtualisation for small DataGridView grids to avoid a re-measure on tab show. #10143 (#10331)

This commit is contained in:
Dave Page committed 2026-10-12 22:58:30 +01:00
1 parent 6302a70316
commit e2e588c324
4 files changed
+148 -10

No files matched your search

@@ -41,6 +41,20 @@ import { FeatureSet } from './features';
import { createGridColumns, GRID_STATE } from './utils';
// The row count above which a grid is worth virtualising. It scales with
// the visible column count rather than being a flat row count, since
// render cost tracks total cells (rows * cols), not rows alone: formula
// and bounds from VIBVEL47's PR #10146. A grid reporting no columns yet
// gets a middling default rather than the 25 row floor, so that a grid
// still settling its columns is not virtualised on the strength of a
// momentary zero.
export function getVirtualiseThreshold(visibleColCount) {
if(!visibleColCount) return 100;
return Math.min(400, Math.max(25, Math.round(700 / visibleColCount)));
}
export default function DataGridView({
field, viewHelperProps, accessPath, dataDispatch, containerClassName
}) {
@@ -119,10 +133,26 @@ export default function DataGridView({
)
).includes(true);
// Virtualising a small grid buys nothing (there's no offscreen window to
// skip rendering) but still pays for measureElement's per-row
// getBoundingClientRect on every mount/remeasure. That remeasure is
// exactly what fires when a dialog tab holding the grid is hidden via
// `display: none` and then shown again, since the scroll viewport
// momentarily measures 0 and the virtualizer's ResizeObserver treats
// that as a real resize. Below the threshold we skip virtualisation
// entirely and render every row in normal document flow, so showing a
// hidden tab is a pure CSS toggle again.
const visibleColCount = table.getVisibleLeafColumns().length;
const virtualiseThreshold = viewHelperProps.virtualiseThreshold ??
getVirtualiseThreshold(visibleColCount);
const shouldVirtualise = rows.length > virtualiseThreshold;
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableEleRef.current,
estimateSize: () => 50,
// Measuring only happens through the row `ref` below, which is left out
// in static mode; that, not this option, is what turns it off there.
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
@@ -149,22 +179,29 @@ export default function DataGridView({
ref={tableEleRef} table={table} data-test="data-grid-view"
tableClassName='DataGridView-table'>
<PgReactTableHeader table={table} />
<PgReactTableBody style={{
height: virtualizer.getTotalSize() + 'px'
}}>
<PgReactTableBody style={
shouldVirtualise ? {height: virtualizer.getTotalSize() + 'px'} : undefined
}>
{
virtualizer.getVirtualItems().map((virtualRow) => {
(
shouldVirtualise
? virtualizer.getVirtualItems()
: rows.map((_row, index) => ({index, start: 0}))
).map((virtualRow) => {
const row = rows[virtualRow.index];
return (
<PgReactTableRow
key={row.id}
data-index={virtualRow.index}
ref={node => virtualizer.measureElement(node)}
style={{
// This should always be a `style` as it changes on
// scroll.
transform: `translateY(${virtualRow.start}px)`,
}}
ref={shouldVirtualise ? node => virtualizer.measureElement(node) : undefined}
className={shouldVirtualise ? undefined : 'pgrt-row--static'}
style={
shouldVirtualise ? {
// This should always be a `style` as it changes
// on scroll.
transform: `translateY(${virtualRow.start}px)`,
} : undefined
}
>
<GridRow
rowId={virtualRow.index} isResizing={isResizing}
@@ -99,6 +99,14 @@ const StyledDiv = styled('div')(({theme})=>({
position: 'absolute',
width: '100%',
// Opted out of the virtualizer's absolute positioning for grids
// small enough that virtualisation isn't used. Keeps the row in
// normal document flow so a hidden/shown dialog tab is a pure CSS
// toggle instead of triggering a virtualizer remeasure.
'&.pgrt-row--static': {
position: 'static',
},
'& .pgrt-row-content': {
display: 'flex',
minHeight: 0,
@@ -172,6 +172,65 @@ describe('SchemaView', ()=>{
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[1], 'rval51');
expect(ctrl.container.querySelector('[data-test="notifier-message"]')).toHaveTextContent('Field5 in FieldColl must be unique.');
});
it('does not virtualise a small grid, rendering rows in static flow', async ()=>{
await simulateValidData();
const dataRows = ctrl.container.querySelectorAll('[data-test="data-table-row"]');
expect(dataRows.length).toBe(2);
// Every row should be fully mounted and opted out of the
// virtualizer's absolute positioning, so a hidden dialog tab is a
// pure CSS toggle rather than something the virtualizer has to
// remeasure when the tab is shown again.
const pgrtRows = ctrl.container.querySelectorAll('.pgrt-row');
expect(pgrtRows.length).toBe(2);
pgrtRows.forEach((rowEl)=>{
expect(rowEl.classList.contains('pgrt-row--static')).toBe(true);
expect(rowEl.style.transform).toBe('');
});
});
it('virtualises a large grid, mounting only a window of rows', async ()=>{
// Clearing the default threshold would need several hundred rows,
// which is slow enough to time out on the Windows runners, so the
// threshold is pinned low instead and the scaling of the default
// is covered directly by getVirtualiseThreshold's own tests.
const manyRows = Array.from({length: 60}, (_, i)=>(
{field3: i, field4: 'field4val', field5: `field5val${i}`}
));
await ctrlMount({
viewHelperProps: {mode: 'create', virtualiseThreshold: 25},
getInitData: ()=>Promise.resolve({fieldcoll: manyRows}),
});
const pgrtRows = ctrl.container.querySelectorAll('.pgrt-row');
expect(pgrtRows.length).toBeGreaterThan(0);
expect(pgrtRows.length).toBeLessThan(manyRows.length);
pgrtRows.forEach((rowEl)=>{
expect(rowEl.classList.contains('pgrt-row--static')).toBe(false);
});
});
it('does not virtualise a grid holding exactly the threshold', async ()=>{
// Virtualisation starts only above the threshold, so a grid of
// exactly that many rows must still render every row statically.
const rowsAtThreshold = Array.from({length: 5}, (_, i)=>(
{field3: i, field4: 'field4val', field5: `field5val${i}`}
));
await ctrlMount({
viewHelperProps: {mode: 'create', virtualiseThreshold: 5},
getInitData: ()=>Promise.resolve({fieldcoll: rowsAtThreshold}),
});
const pgrtRows = ctrl.container.querySelectorAll('.pgrt-row');
expect(pgrtRows.length).toBe(rowsAtThreshold.length);
pgrtRows.forEach((rowEl)=>{
expect(rowEl.classList.contains('pgrt-row--static')).toBe(true);
});
});
});
describe('SQL tab', ()=>{
@@ -0,0 +1,34 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { getVirtualiseThreshold }
from 'sources/SchemaView/DataGridView/grid';
describe('getVirtualiseThreshold', ()=>{
it('scales inversely with the visible column count', ()=>{
expect(getVirtualiseThreshold(4)).toBe(175);
expect(getVirtualiseThreshold(7)).toBe(100);
expect(getVirtualiseThreshold(14)).toBe(50);
});
it('never goes above 400, however few the columns', ()=>{
expect(getVirtualiseThreshold(1)).toBe(400);
expect(getVirtualiseThreshold(2)).toBe(350);
});
it('never goes below 25, however many the columns', ()=>{
expect(getVirtualiseThreshold(28)).toBe(25);
expect(getVirtualiseThreshold(200)).toBe(25);
});
it('uses a middling default when no columns are reported yet', ()=>{
expect(getVirtualiseThreshold(0)).toBe(100);
expect(getVirtualiseThreshold(undefined)).toBe(100);
});
});