[eric] toolui: a data-table renders a 60-row window and one layout; 5,000 rows were 742K of the page's 794K fibers, mounted twice

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit d2bb3c82b948790ec340a886834a3ad9f0109962)
This commit is contained in:
ciregenz
2026-09-06 13:50:52 -07:00
parent 8a991a85cc
commit ae297de242
3 changed files with 129 additions and 32 deletions
@@ -23,6 +23,9 @@ import {
sortData,
createDataTableRowKeys,
getDataTableMobileDescriptionId,
effectiveTableMaxHeight,
pickLayout,
renderedRowCount,
} from "./utilities";
import { renderFormattedValue } from "./formatters";
import type {
@@ -218,6 +221,34 @@ function DataTableLayout({
),
[data, rowIdKey],
);
// One layout mounts, chosen by the measured width; the old pair-and-hide doubled every row.
const hostRef = React.useRef<HTMLDivElement>(null);
const [width, setWidth] = React.useState<number | null>(null);
React.useLayoutEffect(() => {
const el = hostRef.current;
if (!el) return;
setWidth(el.offsetWidth);
if (typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver((entries) => {
const w = entries[0]?.contentRect.width;
if (typeof w === "number") setWidth(w);
});
ro.observe(el);
return () => ro.disconnect();
}, []);
const mode = pickLayout(layout, width);
// Rows are handed out a window at a time; a new payload starts over.
const [windows, setWindows] = React.useState(1);
React.useEffect(() => { setWindows(1); }, [data]);
const shown = renderedRowCount(data.length, windows);
const rows = React.useMemo(() => data.slice(0, shown), [data, shown]);
const shownKeys = React.useMemo(() => rowKeys.slice(0, shown), [rowKeys, shown]);
const hidden = data.length - shown;
const showMore = hidden > 0 ? (
<Button type="button" variant="ghost" size="sm" onClick={() => setWindows((n) => n + 1)} data-slot="data-table-show-more">
Show more rows ({hidden.toLocaleString()} more)
</Button>
) : null;
const mobileDescriptionId = React.useMemo(
() => getDataTableMobileDescriptionId(String(id ?? "data-table")),
[id],
@@ -233,20 +264,14 @@ function DataTableLayout({
return (
<div
ref={hostRef}
className={cn("@container w-full min-w-80", className)}
data-tool-ui-id={id}
data-slot="data-table"
data-layout={layout}
data-layout={mode}
>
<div
className={cn(
layout === "table"
? "block"
: layout === "cards"
? "hidden"
: "hidden @md:block",
)}
>
{mode === "table" && (
<div className="block">
<div className="relative">
<div
className={cn(
@@ -277,21 +302,20 @@ function DataTableLayout({
{data.length === 0 ? (
<DataTableEmpty message={emptyMessage} />
) : (
<DataTableContent />
<DataTableContent rows={rows as unknown as DataTableRowData[]} rowKeys={shownKeys} footer={showMore ? (
<TableRow>
<TableCell colSpan={Math.max(1, columns.length)} className="py-1 text-center">{showMore}</TableCell>
</TableRow>
) : null} />
)}
</Table>
</div>
</div>
</div>
)}
{mode === "cards" && (
<div
className={cn(
layout === "cards"
? ""
: layout === "table"
? "hidden"
: "@md:hidden",
)}
role="list"
aria-label="Data table (mobile card view)"
aria-describedby={mobileDescriptionId}
@@ -308,8 +332,8 @@ function DataTableLayout({
</div>
) : (
<div className="bg-card flex flex-col overflow-hidden rounded-2xl border shadow-xs">
{data.map((row, i) => {
const rowKey = rowKeys[i];
{rows.map((row, i) => {
const rowKey = shownKeys[i];
return (
<DataTableAccordionCard
key={rowKey}
@@ -320,9 +344,11 @@ function DataTableLayout({
/>
);
})}
{showMore && <div className="border-t py-1 text-center">{showMore}</div>}
</div>
)}
</div>
)}
{sortAnnouncement && (
<div className="sr-only" aria-live="polite">
@@ -397,11 +423,17 @@ export const DataTable = Object.assign(DataTableRoot, {
Provider: DataTableProvider,
}) as DataTableComponent;
function DataTableContent() {
interface DataTableContentProps {
rows: DataTableRowData[];
rowKeys: string[];
footer: React.ReactNode;
}
function DataTableContent({ rows, rowKeys, footer }: DataTableContentProps) {
return (
<>
<DataTableHeader />
<DataTableBody />
<DataTableBody rows={rows} rowKeys={rowKeys} footer={footer} />
</>
);
}
@@ -637,16 +669,8 @@ function DataTableHead({
);
}
function DataTableBody() {
function DataTableBody({ rows, rowKeys, footer }: DataTableContentProps) {
const { data, rowIdKey } = useDataTable<DataTableRowData>();
const rowKeys = React.useMemo(
() =>
createDataTableRowKeys(
data as Array<Record<string, unknown>>,
rowIdKey ? String(rowIdKey) : undefined,
),
[data, rowIdKey],
);
const hasWarnedRowKeyRef = React.useRef(false);
React.useEffect(() => {
@@ -665,10 +689,11 @@ function DataTableBody() {
return (
<TableBody>
{data.map((row, index) => {
{rows.map((row, index) => {
const rowKey = rowKeys[index];
return <DataTableRow key={rowKey} row={row} />;
})}
{footer}
</TableBody>
);
}
@@ -0,0 +1,34 @@
// Run: npm test (frontend/scripts/run-tests.mjs)
//
// A 5,000-row data-table in one expanded chat was 742,090 of the page's 793,776 React fibers (census,
// 2026-09-05): every row rendered, and twice, because the auto layout mounted the table AND the card view
// and hid one with a container query. Rows are now handed out a window at a time and one layout mounts.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { renderedRowCount, pickLayout, RENDER_ROW_WINDOW, CARD_LAYOUT_MAX_WIDTH } from './utilities.ts';
test('rows come in windows; a short table is whole, a long one is one window until asked for more', () => {
assert.equal(renderedRowCount(5, 1), 5);
assert.equal(renderedRowCount(5000, 1), RENDER_ROW_WINDOW);
assert.equal(renderedRowCount(5000, 2), 2 * RENDER_ROW_WINDOW);
assert.equal(renderedRowCount(70, 2), 70);
assert.equal(renderedRowCount(5000, 0), RENDER_ROW_WINDOW, 'zero windows still shows the first');
});
test('one layout is picked from the measured width; unmeasured is the wide one, explicit layouts are honoured', () => {
assert.equal(pickLayout('auto', null), 'table');
assert.equal(pickLayout('auto', CARD_LAYOUT_MAX_WIDTH), 'table');
assert.equal(pickLayout('auto', CARD_LAYOUT_MAX_WIDTH - 1), 'cards');
assert.equal(pickLayout('cards', 2000), 'cards');
assert.equal(pickLayout('table', 100), 'table');
});
test('the component maps the windowed rows, never the whole payload, and no longer mounts the hidden twin', () => {
const src = readFileSync(resolve(process.cwd(), 'src/toolui/components/data-table/data-table.tsx'), 'utf8');
assert.doesNotMatch(src, /@md:(block|hidden)/, 'the pair-and-hide container query is what doubled every row');
assert.match(src, /<TableBody>\s*\{rows\.map\(/, 'the table body maps the window');
assert.doesNotMatch(src, /\{data\.map\(\(row/, 'nothing maps the full payload into the DOM');
assert.match(src, /renderedRowCount\(data\.length, windows\)/);
});
@@ -297,3 +297,41 @@ export function parseNumericLike(input: string): number | null {
}
return null;
}
// Long tables used to render every row, so a "collapsed" card could tower over the expanded one.
export const DEFAULT_ROW_CAP = 6;
// Roughly a header plus six rows at the table's default density; past it the body scrolls in place.
export const DEFAULT_CAPPED_MAX_HEIGHT = "312px";
/** The height cap the scroll container actually uses: a user's drag beats the payload's explicit maxHeight, which beats the row-count default. */
export function effectiveTableMaxHeight(
explicit: string | undefined,
userPx: number | null,
rowCount: number,
): string | undefined {
if (userPx != null) return `${userPx}px`;
if (explicit) return explicit;
return rowCount > DEFAULT_ROW_CAP ? DEFAULT_CAPPED_MAX_HEIGHT : undefined;
}
// Rendering every row is the cost, not showing it: a 5,000-row payload put 370,000 React fibers under ONE
// widget, and twice over, because the auto layout mounted the table and the card view together and hid one
// with a container query (census 2026-09-05: 742,090 of the page's 793,776 fibers). A window is what a reader
// reaches by scrolling the capped body a few times; a "Show more" row hands out the next one.
export const RENDER_ROW_WINDOW = 60;
export function renderedRowCount(total: number, windows: number): number {
return Math.min(total, Math.max(1, windows) * RENDER_ROW_WINDOW);
}
// The container query flipped to cards under --container-md; the same number now decides which ONE layout mounts.
export const CARD_LAYOUT_MAX_WIDTH = 448;
export type ResolvedTableLayout = "table" | "cards";
/** Unmeasured (first paint, no ResizeObserver) reads as the wide layout; a narrow container flips to cards on the same frame. */
export function pickLayout(layout: "auto" | ResolvedTableLayout, widthPx: number | null): ResolvedTableLayout {
if (layout !== "auto") return layout;
if (widthPx == null) return "table";
return widthPx < CARD_LAYOUT_MAX_WIDTH ? "cards" : "table";
}