diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx
index 8366cd44ccd..0147ad79ca9 100644
--- a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx
+++ b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx
@@ -46,6 +46,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
}) {
@@ -122,12 +136,27 @@ 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,
measureElement:
- typeof window !== 'undefined' &&
+ shouldVirtualise &&
+ typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? element => element?.getBoundingClientRect().height
: undefined,
@@ -152,22 +181,29 @@ export default function DataGridView({
ref={tableEleRef} table={table} data-test="data-grid-view"
tableClassName='DataGridView-table'>
-
+
{
- virtualizer.getVirtualItems().map((virtualRow) => {
+ (
+ shouldVirtualise
+ ? virtualizer.getVirtualItems()
+ : rows.map((_row, index) => ({index, start: 0}))
+ ).map((virtualRow) => {
const row = rows[virtualRow.index];
return (
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
+ }
>
({
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,
diff --git a/web/regression/javascript/SchemaView/SchemaDialogView.spec.js b/web/regression/javascript/SchemaView/SchemaDialogView.spec.js
index 9c4c944138e..fd4eb33c626 100644
--- a/web/regression/javascript/SchemaView/SchemaDialogView.spec.js
+++ b/web/regression/javascript/SchemaView/SchemaDialogView.spec.js
@@ -172,6 +172,46 @@ 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);
+ });
+ });
});
describe('SQL tab', ()=>{
diff --git a/web/regression/javascript/SchemaView/getVirtualiseThreshold.spec.js b/web/regression/javascript/SchemaView/getVirtualiseThreshold.spec.js
new file mode 100644
index 00000000000..77e55b1c2a9
--- /dev/null
+++ b/web/regression/javascript/SchemaView/getVirtualiseThreshold.spec.js
@@ -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);
+ });
+});