double-click to edit · drag to select · ⌘C copy · ⌘V paste into Qty · funnel to filter · drag a header up to group
double-click to edit · drag to select · ⌘C copy · ⌘V paste into Qty · funnel to filter · drag a header up to group
$ pretable — vol. 2 · no. 1
60fps under live market load. Zero row drift while an AI analyst streams wrapped commentary beside ticking prices — the grid built for live, AI-augmented data, not retrofitted from a batch-era table.
MIT licensed · open source
Demo uses illustrative, synthetic market data — not investment advice.
02 · how it works
Wrapped row heights are computed with character-width tables and font metrics — pure arithmetic. No getBoundingClientRect, no forced reflow, no measure-on-mount. The DOM is touched exactly once per frame, at commit. The five-stage pipeline below is what enforces that discipline.
Streaming patches and static rows treated identically.
Pure reducer. Sort, filter, selection, row-id stability.
Row-height plan + virtualization range. Off-DOM measurement.
The only stage that touches the DOM.
RAF coalesces patches per animation frame.
LLM streams, partial JSON, tool-call traces — bursts of 100 to 25,000 patches/sec all collapse to one snapshot per animation frame. No per-token reflow. Selection survives every patch.
(rows, columns, sort, filter, selection) → Snapshot. No imperative DOM. Streaming patches and batch arrays hit the same reducer — that's why selection survives every update.
100 to 25,000 patches/sec all collapse to one snapshot per animation frame. Long tasks: zero across the operating envelope.
Render counts, viewport range, planned height — all data emitted by the engine, never read from the DOM. Zero measurement-induced thrash.
↳ Read the source: packages/grid-core, layout-core, text-core, renderer-dom — under 3,000 lines combined.
03 · how we compare
Wrapped-text scroll at 3,000 rows on Chromium, against the three most-cited React grids. Pretable matches MUI at the front, ~1.7× ahead of AG Grid Community and TanStack on raw frame p95 — and is the only adapter here that clears every quality threshold (zero blank gaps, zero anchor shift, ≤1 px row-height drift) at full-grid feature weight. Interactive sort and filter run 2–3.5× faster than every measured comparator on the same dataset. See the methodology →
| metric | pretable | AG Grid | TanStack | MUI X | budget |
|---|---|---|---|---|---|
| frame p95 (ms) — wrapped scroll | 9.07 | 16.7 | 16.7 | 9.14 | ≤ 16 |
| row-height fidelity (px error) | 1 | 2 | 0 | 1 | ≤ 1 |
| blank gaps under scroll | 0 | 1 | 1 | 0 | 0 |
| scroll anchor shift (px) | 0 | 0 | 0 | 0 | ≤ 16 |
| sort latency p95 (ms) — interaction | 17.1 | 58.3 | 34.4 | 35.0 | — |
| filter-metadata latency p95 (ms) | 17.5 | 49.9 | 15.7 | 33.4 | — |
| filter-text latency p95 (ms) | 16.8 | 50.0 | 40.2 | 33.3 | — |
| headless engine + React surface | yes | n/a | engine only | n/a | — |
| streaming pipeline (SSE → partial JSON → batcher → applyTransaction) | yes | n/a | n/a | n/a | — |
05 · streaming, by design
Most grids accept streaming through an adapter layered onto a batch-era data model. Pretable's engine treats a 1,000-patch/sec stream and a static 3,000-row array through the same indexed render and selection model. There's no "streaming mode" toggle.
Ordinary application data stays simple: pass a rows prop. High-frequency producers explicitly own a row model; the streaming adapter RAF-batches its transactions. Both feed the same indexed rendering and selection model.
Row-id keys are first-class in the engine. Sort, filter, scroll position, focused row — none of it loses state mid-stream. Drag-select 200 rows during a 25k/sec patch storm and they stay selected.
06 · for engineers
Connect any token-streaming source — OpenAI Responses, Anthropic, or your own SSE — to a pretable grid. Selection survives every chunk.
Turn a streaming LLM response into rows with connectElementStream and append them to the grid as they arrive.
"use client";
import { connectElementStream } from "@pretable/stream-adapter";
import { PretableSurface, useDisposeOnUnmount } from "@pretable/react";
import { createLocalRowModel } from "@pretable/core";
import { useEffect, useMemo } from "react";
import { columns, type ChatRow } from "./columns";
import {
responseEventsToChatRows,
type ChatResponseEvent,
} from "./response-events-to-chat-rows";
export type OpenChatResponseEvents = (input: {
readonly model: string;
readonly prompt: string;
}) =>
AsyncIterable<ChatResponseEvent> | Promise<AsyncIterable<ChatResponseEvent>>;
export function ChatGrid({
prompt,
openResponseEvents,
}: {
prompt: string;
openResponseEvents: OpenChatResponseEvents;
}) {
const rowModel = useMemo(
() => createLocalRowModel({ rows: [], columns, getRowId: (row) => row.id }),
[],
);
useEffect(() => {
let disposed = false;
let connection: ReturnType<typeof connectElementStream> | undefined;
void (async () => {
const stream = await openResponseEvents({
model: "gpt-5",
prompt,
});
const rows: AsyncIterable<ChatRow> = responseEventsToChatRows(stream);
connection = connectElementStream(rowModel, rows);
if (disposed) connection.dispose();
})();
return () => {
disposed = true;
connection?.dispose();
};
}, [openResponseEvents, prompt, rowModel]);
// NOT `useEffect(() => () => rowModel.dispose())`: StrictMode rehearses an
// unmount in dev, `useMemo` hands the same model back to the remount, and the
// grid then renders nothing at all. `useDisposeOnUnmount` defers the disposal
// by a microtask so a remount can cancel it.
useDisposeOnUnmount(rowModel);
return (
<PretableSurface
ariaLabel="Streaming chat"
model={rowModel}
viewportHeight={320}
/>
);
}Full reference: /docs/streaming
07 · what's in the box
Each feature backed by a bench scenario or demo. No claim without a click-to-prove.
Sub-frame scroll p95 on wrapped text — at parity with the best full-grid comparator (MUI X) and ~1.7× ahead of AG Grid Community and TanStack. Zero blank gaps, zero anchor shift, ≤1 px row-height drift.
Multi-line cells, variable row heights, smooth scrolling. Multilingual content tested.
Token-by-token rendering for OpenAI, Anthropic, and your own SSE. The full pipeline — partial-JSON parser, frame-budget batcher, applyTransaction wiring — ships as one import.
Filter, sort, and reorder without losing your selection. Stable focus across mutations.
08 · scale
Pretable virtualizes both axes. The grid below holds 1.25 million cells; scroll anywhere and the live counter shows how few actually exist in the DOM at once — matching our published 2,500 × 500 benchmark (~160 peak nodes).
09 · columns, your way
Drag a column border to resize, drag a header to reorder — no config, no plugins. Make a mess, then hit reset.
Get started