Skip to content

Instantly share code, notes, and snippets.

@zax4r0
Last active October 15, 2025 18:10
Show Gist options
  • Select an option

  • Save zax4r0/5300b8ac51316ae962e4529b911bea7e to your computer and use it in GitHub Desktop.

Select an option

Save zax4r0/5300b8ac51316ae962e4529b911bea7e to your computer and use it in GitHub Desktop.
cell.ts
// FastGrid.tsx (main component)
import clsx from "clsx";
import Stats from "stats.js";
import { Analytics } from "@vercel/analytics/react";
import { FilterCell, Grid, type CellRenderer } from "grid";
import { COLUMNS, generateRows } from "./generateRows";
import React, { useState, useRef, useEffect } from "react";
// Human-friendly custom React cell renderer: Highlights numeric values > 50 in green for the "amount" column (index 4)
// For other columns, just displays the value cleanly. Easy to extend with colIndex logic.
const MyCustomReactCellComponent: React.FC<{ value: string | number; colIndex: number }> = ({ value, colIndex }) => {
// Example: Only apply highlighting logic to column 4 (assuming it's the numeric "amount" column)
let displayValue = String(value);
let textClass = "text-gray-800";
let extraContent = null;
if (colIndex === 4) {
const numValue = typeof value === 'number' ? value : parseFloat(value as string);
const isHighlighted = !isNaN(numValue) && numValue > 50;
if (isHighlighted) {
textClass = "text-green-600 font-semibold";
extraContent = <span className="ml-1 text-green-500">✓</span>; // Simple icon for highlight
}
}
// For other columns, you could add more colIndex-specific logic here, e.g.:
// if (colIndex === 3) { displayValue = value.toUpperCase(); } // e.g., uppercase emails
return (
<div className="flex items-center w-full h-full">
<span className={clsx("font-mono text-[14px]", textClass)}>
{displayValue}
</span>
{extraContent}
</div>
);
};
export const FastGrid = () => {
const containerRef = useRef<HTMLDivElement>(null);
const [grid, setGrid] = useState<Grid | null>(null);
const [speed, setSpeed] = useState(0);
const [rowCount, setRowCount] = useState(100_000);
const [stressTest, setStressTest] = useState(false);
const [loadingRows, setLoadingRows] = useState(false);
const [autoScroller, setAutoScroller] = useState<AutoScroller | null>(null);
// Human-friendly API: Define custom renderers by column name (from COLUMNS array)
// Example: const customRenderersByName: Record<string, CellRenderer> = { [COLUMNS[4]]: MyCustomReactCellComponent }; // Only for "amount"
// To apply to specific columns: if (customRenderersByName[colName]) acc[index] = customRenderersByName[colName];
// Here, applying the same custom React cell to ALL columns for demonstration
const customRenderersByName: Record<string, CellRenderer> = COLUMNS.reduce((acc, colName) => {
acc[colName] = MyCustomReactCellComponent;
return acc;
}, {} as Record<string, CellRenderer>);
// Map by-name renderers to by-index for the Grid (keeps internal API simple)
const customColumnRenderers: Record<number, CellRenderer> = COLUMNS.reduce((acc, colName, index) => {
const renderer = customRenderersByName[colName];
if (renderer) {
acc[index] = renderer;
}
return acc;
}, {} as Record<number, CellRenderer>);
useEffect(() => {
const container = containerRef.current;
if (container == null) {
return;
}
// init grid
const t0 = performance.now();
// Pass customColumnRenderers to enable React cells (defaults to {} if omitted)
const grid = new Grid(container, [], COLUMNS, customColumnRenderers);
setGrid(grid);
console.info("Ms to intitialize grid:", performance.now() - t0);
setLoadingRows(true);
generateRows(rowCount, grid, () => setLoadingRows(false));
// setup autoscroller
const autoScroller = new AutoScroller(grid);
setAutoScroller(autoScroller);
(window as any).__grid = grid;
return () => {
grid.destroy();
};
}, [rowCount]);
useEffect(() => {
if (grid == null || !stressTest) return;
const id = setInterval(() => {
const filters = grid.rowManager.view.filter;
if (filters[4] == null || filters[4].length < 5) {
filters[4] =
(filters[4] ?? "") + Math.floor(Math.random() * 10).toString();
} else {
delete filters[4];
}
// manually trigger refresh of filter cells.. make it part of updating the filter
for (const header of grid.headerRows) {
for (const cell of Object.values(header.cellComponentMap)) {
if (cell instanceof FilterCell) {
if (cell.index === 4) {
cell.el.style.backgroundColor = "rgb(239, 68, 68)";
cell.input.style.backgroundColor = "rgb(239, 68, 68)";
cell.input.style.color = "white";
cell.input.placeholder = "";
cell.arrow.style.fill = "white";
cell.syncToFilter();
}
}
}
}
grid.rowManager.runFilter();
}, 333);
return () => {
// manually trigger refresh of filter cells.. make it part of updating the filter
for (const header of grid.headerRows) {
for (const cell of Object.values(header.cellComponentMap)) {
if (cell instanceof FilterCell) {
if (cell.index === 4) {
delete grid.rowManager.view.filter[4];
cell.el.style.backgroundColor = "white";
cell.input.style.backgroundColor = "white";
cell.input.style.color = "black";
cell.input.placeholder = "filter...";
cell.arrow.style.fill = "black";
cell.syncToFilter();
}
}
}
}
grid.rowManager.runFilter();
clearInterval(id);
};
}, [grid, stressTest]);
useEffect(() => {
if (autoScroller == null) return;
autoScroller.start(speed === 0 ? 0 : Math.exp(speed / 15));
}, [autoScroller, speed]);
return (
<>
<Analytics />
<h1 className="self-start text-lg font-bold sm:self-center md:text-3xl">
World's most performant DOM-based table
</h1>
<div className="mt-1 self-start max-md:mt-2 sm:self-center">
Try make the fps counter drop by filtering, sorting, and scrolling
simultaneously
</div>
<div className="mb-4 mt-1 self-start text-sm max-md:mt-2 sm:self-center sm:text-[13px]">
See code:
<a
className="ml-1 text-blue-600 underline hover:text-blue-800"
href="https://github.com/gabrielpetersson/fast-grid/"
>
https://github.com/gabrielpetersson/fast-grid/
</a>
</div>
<div
className={clsx(
"flex w-full select-none flex-wrap justify-between gap-2 py-2",
loadingRows && "pointer-events-none select-none opacity-60"
)}
>
<div className="hidden w-[150px] md:block" />
<div className="flex gap-2 text-[11px] md:gap-8 md:text-[13px]">
<div className="flex items-center">
<span className="mr-2 whitespace-nowrap">Scroll speed:</span>
<input
type="range"
min="0"
max="100"
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
className={clsx(
"h-2 w-full cursor-pointer appearance-none rounded-full bg-gray-300",
speed === 100 &&
"[&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:bg-red-500 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-red-500"
)}
/>
</div>
<button
className={clsx(
"flex h-[28px] w-[200px] items-center justify-center gap-0.5 rounded bg-blue-500 text-white hover:opacity-95 active:opacity-90",
stressTest && "bg-red-500"
)}
onClick={() => {
if (stressTest) {
setStressTest(false);
setSpeed(0);
} else {
setStressTest(true);
setSpeed(100);
}
}}
>
{stressTest && (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-[14px] w-[14px]"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
)}
{stressTest ? "Filtering 3 times per second" : "Stress test"}
</button>
</div>
<select
value={rowCount}
onChange={(e) => {
if (grid == null) return;
setRowCount(Number(e.target.value));
}}
className="hidden h-[28px] w-[150px] items-center justify-center rounded border border-gray-800 bg-white text-[12px] text-gray-700 shadow-[rgba(0,_0,_0,_0.1)_0px_0px_2px_1px] md:flex"
>
<option value={10}>10 rows</option>
<option value={10_000}>10 000 rows</option>
<option value={100_000}>100 000 rows</option>
<option value={200_000}>200 000 rows</option>
<option value={500_000}>500 000 rows</option>
<option value={1_000_000}>
1 000 000 rows (might run out of ram)
</option>
<option value={2_000_000}>
2 000 000 rows (might run out of ram)
</option>
<option value={5_000_000}>
5 000 000 rows (might run out of ram)
</option>
<option value={10_000_000}>
10 000 000 rows (might run out of ram)
</option>
</select>
</div>
<div
ref={containerRef} // attaching grid here
style={{
contain: "strict",
}}
className={clsx(
"relative box-border w-full flex-1 overflow-clip border border-gray-700 bg-white",
loadingRows && "pointer-events-none opacity-70"
)}
/>
</>
);
};
const setupFPS = () => {
const stats = new Stats();
stats.showPanel(0);
stats.dom.style.top = "unset";
stats.dom.style.left = "unset";
stats.dom.style.bottom = "0";
stats.dom.style.right = "0";
for (const child of stats.dom.children) {
// @ts-expect-error ddd
child.style.width = "160px";
// @ts-expect-error ddd
child.style.height = "96px";
}
document.body.appendChild(stats.dom);
const animate = () => {
stats.update();
window.requestAnimationFrame(animate);
};
window.requestAnimationFrame(animate);
};
setupFPS();
class AutoScroller {
grid: Grid;
running: boolean;
toBottom: boolean;
version: number;
constructor(grid: Grid) {
this.grid = grid;
this.running = true;
this.toBottom = true;
this.version = 0;
}
start(speed: number) {
this.version++;
const currentVersion = this.version;
const cb = () => {
const state = this.grid.getState();
if (this.version !== currentVersion) {
return;
}
if (
this.grid.offsetY >
state.tableHeight - this.grid.viewportHeight - 1
) {
this.toBottom = false;
} else if (this.grid.offsetY <= 0) {
this.toBottom = true;
}
const delta = this.toBottom ? speed : -speed;
const wheelEvent = new WheelEvent("wheel", {
deltaY: delta,
deltaMode: 0,
});
this.grid.container.dispatchEvent(wheelEvent);
window.requestAnimationFrame(cb);
};
window.requestAnimationFrame(cb);
}
}
import { Grid } from "./grid";
export const CELL_WIDTH = 200;
export type CellComponent = {
id: number;
el: HTMLDivElement;
_offset: number;
setContent: (text: string | number) => void;
setOffset: (offset: number, force?: boolean) => void;
reuse: (
id: number,
offset: number,
text: string | number,
index: number
) => void;
};
export class StringCell implements CellComponent {
id: number;
el: HTMLDivElement;
_offset: number;
constructor(id: number, offset: number, text: string | number) {
this.id = id;
this._offset = offset;
this.el = document.createElement("div");
// NOTE(gab): fonts are super expensive, might be more simple fonts that are faster to render? testing to render a cursive text with subpixel antialiasing, vs
// rendering monospace text with text smoothing
// https://codesandbox.io/s/performance-test-disabling-text-antialiasing-om6f3q?file=/index.js
// NOTE(gab): align-items actually has a super slight imapact on Layerize time, using padding for now
this.el.className =
"flex h-full pt-[7px] border-[0] border-r border-b border-solid border-gray-700 text-gray-800 box-border cursor-default pl-[6px] absolute left-0 font-mono text-[14px]";
this.el.style.width = `${CELL_WIDTH}px`;
this.setOffset(this._offset, true);
this.setContent(text);
}
setContent(text: string | number) {
this.el.innerText = String(text);
}
setOffset(offset: number, force: boolean = false) {
if (force || offset !== this._offset) {
this.el.style.transform = `translateX(${offset}px)`;
}
this._offset = offset;
}
reuse(id: number, offset: number, text: string | number) {
this.id = id;
this.setOffset(offset, true);
this.setContent(text);
}
}
export class HeaderCell implements CellComponent {
id: number;
el: HTMLDivElement;
_offset: number;
constructor(id: number, offset: number, text: string | number) {
this.id = id;
this._offset = offset;
this.el = document.createElement("div");
this.el.className =
"flex h-full pt-[5px] border-[0] border-r border-b-2 border-solid border-gray-700 text-gray-800 box-border cursor-default pl-[6px] absolute left-0 overflow-clip";
this.el.style.width = `${CELL_WIDTH}px`;
// extra header styles
this.el.style.backgroundColor = "white";
this.el.style.fontWeight = "500";
this.el.style.fontFamily = "monospace";
this.el.style.fontSize = "15px";
this.setOffset(this._offset, true);
this.setContent(text);
}
setContent(text: string | number) {
this.el.innerText = String(text);
}
setOffset(offset: number, force: boolean = false) {
if (force || offset !== this._offset) {
this.el.style.transform = `translateX(${offset}px)`;
}
this._offset = offset;
}
reuse(id: number, offset: number, text: string | number) {
this.id = id;
this.setOffset(offset, true);
this.setContent(text);
}
}
export class FilterCell implements CellComponent {
grid: Grid;
index: number;
id: number;
el: HTMLDivElement;
input: HTMLInputElement;
arrow: SVGSVGElement;
_offset: number;
constructor(
id: number,
offset: number,
text: string | number,
grid: Grid,
index: number
) {
this.grid = grid;
this.index = index;
this.id = id;
this._offset = offset;
this.el = document.createElement("div");
this.el.className =
"flex h-full pt-[5px] border-[0] border-r border-b border-solid border-gray-700 text-gray-800 box-border cursor-default pl-[6px] absolute left-0 overflow-clip";
this.el.style.width = `${CELL_WIDTH}px`;
this.input = document.createElement("input");
this.input.type = "text";
this.input.value = String(text);
this.input.className =
"w-full h-full border-none outline-none text-[13px] select-none";
this.input.style.fontFamily = "monospace";
this.input.placeholder = "filter...";
this.input.addEventListener("input", this.onInputChange);
this.el.appendChild(this.input);
const arrowContainer = document.createElement("div");
arrowContainer.className =
"flex items-center justify-center w-[35px] h-[28px] cursor-pointer";
arrowContainer.addEventListener("click", this.onArrowClick);
this.arrow = document.createElementNS("http://www.w3.org/2000/svg", "svg");
this.arrow.setAttribute("xmlns", "http://www.w3.org/2000/svg");
this.arrow.setAttribute("viewBox", "0 0 24 24");
this.arrow.setAttribute(
"class",
"w-5 h-5 fill-current transition-transform duration-200 rotate-90"
);
const mainPath = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
mainPath.setAttribute(
"d",
"M12 3.75l-6.5 6.5L7 11.75l3.5-3.5V20h3V8.25l3.5 3.5 1.5-1.5z"
);
this.arrow.appendChild(mainPath);
const arrowHead = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
arrowHead.setAttribute("d", "M12 5.5L7.5 10h9z");
arrowHead.setAttribute("fill", "currentColor");
arrowHead.setAttribute("opacity", "0.3");
this.arrow.appendChild(arrowHead);
arrowContainer.appendChild(this.arrow);
this.el.appendChild(arrowContainer);
this.syncToFilter();
this.setOffset(this._offset, true);
}
private onInputChange = () => {
if (this.input.value === "") {
delete this.grid.rowManager.view.filter[this.index];
} else {
this.grid.rowManager.view.filter[this.index] = this.input.value;
}
this.grid.rowManager.runFilter();
};
private onArrowClick = () => {
const idx = this.grid.rowManager.view.sort.findIndex(
(sort) => sort.column === this.index
);
const currentSort = idx !== -1 ? this.grid.rowManager.view.sort[idx] : null;
if (currentSort == null) {
this.grid.rowManager.view.sort.push({
direction: "descending",
column: this.index,
});
this.arrow.style.transform = "rotate(180deg)";
} else if (currentSort.direction === "descending") {
currentSort.direction = "ascending";
this.arrow.style.transform = "rotate(0deg)";
} else {
this.grid.rowManager.view.sort.splice(idx, 1);
this.arrow.style.transform = "rotate(90deg)";
}
this.grid.rowManager.runSort();
};
syncToFilter = () => {
if (this.index in this.grid.rowManager.view.filter) {
this.input.value = this.grid.rowManager.view.filter[this.index];
} else {
this.input.value = "";
}
if (this.index in this.grid.rowManager.view.sort) {
const sort = this.grid.rowManager.view.sort.find(
(sort) => sort.column === this.index
);
if (sort == null) {
this.arrow.style.transform = "rotate(90deg)";
} else if (sort.direction === "descending") {
this.arrow.style.transform = "rotate(180deg)";
} else {
this.arrow.style.transform = "rotate(0deg)";
}
}
};
setContent = () => {
this.syncToFilter();
};
setOffset = (offset: number, force: boolean = false) => {
if (force || offset !== this._offset) {
this.el.style.transform = `translateX(${offset}px)`;
}
this._offset = offset;
};
reuse = (
id: number,
offset: number,
_text: string | number,
index: number
) => {
this.id = id;
this.index = index;
this.setOffset(offset, true);
this.syncToFilter();
};
}
import React from "react";
import { createRoot, Root } from "react-dom/client";
export class ReactCellWrapper implements CellComponent {
id: number;
el: HTMLDivElement;
_offset: number;
private root: Root;
private reactComponent: React.ComponentType<{ value: string | number; colIndex: number }>;
private colIndex: number;
constructor(
reactComponent: React.ComponentType<{ value: string | number; colIndex: number }>,
id: number,
offset: number,
text: string | number,
colIndex: number
) {
this.reactComponent = reactComponent;
this.id = id;
this.colIndex = colIndex;
this._offset = offset;
this.el = document.createElement("div");
// Reusing styles from StringCell for visual consistency in data cells
this.el.className =
"flex h-full pt-[7px] border-[0] border-r border-b border-solid border-gray-700 text-gray-800 box-border cursor-default pl-[6px] absolute left-0 font-mono text-[14px]";
this.el.style.width = `${CELL_WIDTH}px`;
this.root = createRoot(this.el);
this.setOffset(offset, true);
this.setContent(text);
}
setContent(text: string | number) {
this.root.render(
React.createElement(this.reactComponent, {
value: text,
colIndex: this.colIndex,
})
);
}
setOffset(offset: number, force: boolean = false) {
if (force || offset !== this._offset) {
this.el.style.transform = `translateX(${offset}px)`;
}
this._offset = offset;
}
reuse(id: number, offset: number, text: string | number, index: number) {
this.id = id;
this.colIndex = index;
this.setOffset(offset, true);
this.setContent(text);
}
destroy() {
// Clean up the React root to prevent memory leaks on cell reuse/removal
this.root.unmount();
}
}
export type CellRenderer =
| typeof StringCell
| typeof HeaderCell
| typeof FilterCell
| React.ComponentType<{ value: string | number; colIndex: number }>;
import {
CELL_WIDTH,
CellComponent,
HeaderCell,
FilterCell,
StringCell,
ReactCellWrapper,
} from "./cell";
import { Grid } from "./grid";
export type Cell = {
id: number;
v: string | number;
// val: number;
};
export interface Row {
id: number;
cells: Cell[];
}
type CellRenderer = typeof StringCell | typeof HeaderCell | typeof FilterCell;
export class RowComponent {
id: number;
el: HTMLDivElement;
cells: Cell[];
_offset: number;
defaultCellRenderer: CellRenderer; // Renamed for clarity; this is the fallback for columns without specific renderers
columnCellRenderers?: Record<number, CellRenderer>; // Optional per-column overrides, including React components
cellComponentMap: Record<string, CellComponent>;
grid: Grid;
constructor(
grid: Grid,
id: number,
cells: Cell[],
offset: number,
defaultCellRenderer: CellRenderer, // Made explicit as default
columnCellRenderers?: Record<number, CellRenderer> // New: allows per-column custom renderers (vanilla or React)
) {
this.grid = grid;
this.id = id;
this.cells = cells;
this._offset = offset;
this.defaultCellRenderer = defaultCellRenderer;
this.columnCellRenderers = columnCellRenderers;
this.cellComponentMap = {};
this.el = document.createElement("div");
this.el.className = "absolute top-0 h-[32px]";
// eh temporary header hack, make this passable
if (defaultCellRenderer !== StringCell) {
this.el.style.zIndex = "1";
}
this.setOffset(this._offset, true);
this.renderCells();
}
destroy() {
// TODO(gab): can speed be improved?
// https://github.com/brianmhunt/knockout-fast-foreach/issues/37
// TODO(gab): should not need this, but crashes on my other computer otherwise. check
for (const [id, cell] of Object.entries(this.cellComponentMap)) {
cell.destroy?.(); // Call destroy if available (e.g., for React cells)
delete this.cellComponentMap[id];
}
if (this.grid.container.contains(this.el)) {
this.grid.container.removeChild(this.el);
} else {
console.error("row component already removed");
}
}
setOffset(offset: number, force: boolean = false) {
if (force || offset != this._offset) {
this.el.style.transform = `translateY(${offset}px)`;
}
this._offset = offset;
}
renderCells() {
const state = this.grid.getState();
const renderCells: Record<string, true> = {};
for (let i = state.startCell; i < state.endCell; i++) {
const cell = this.cells[i];
renderCells[cell.id] = true;
}
const removeCells: CellComponent[] = [];
for (const id in this.cellComponentMap) {
if (id in renderCells) {
continue;
}
const cell = this.cellComponentMap[id]!;
removeCells.push(cell);
}
for (let i = state.startCell; i < state.endCell; i++) {
const cell = this.cells[i]!;
const offset = state.cellOffset + (i - state.startCell) * CELL_WIDTH;
const renderer = this.columnCellRenderers
? this.columnCellRenderers[i] || this.defaultCellRenderer
: this.defaultCellRenderer;
// Determine if this is a vanilla class-based renderer or a React component
const prototype = (renderer as any).prototype;
const isVanilla = prototype && "setContent" in prototype;
const isFilter = renderer === FilterCell;
const existingCell = this.cellComponentMap[cell.id];
if (existingCell != null) {
existingCell.setOffset(offset);
continue;
}
const reuseCell = removeCells.pop();
if (reuseCell != null) {
delete this.cellComponentMap[reuseCell.id];
if (isVanilla) {
// Vanilla reuse always passes index for consistency
(reuseCell as any).reuse(cell.id, offset, cell.v, i);
} else {
// React wrapper reuse
(reuseCell as ReactCellWrapper).reuse(cell.id, offset, cell.v, i);
}
this.cellComponentMap[cell.id] = reuseCell;
continue;
}
let newCell: CellComponent;
if (!isVanilla) {
// Create React wrapper for custom React cells
newCell = new ReactCellWrapper(
renderer as React.ComponentType<{ value: string | number; colIndex: number }>,
cell.id,
offset,
cell.v,
i
);
} else {
// Vanilla cell creation
const args = [cell.id, offset, cell.v];
if (isFilter) {
args.push(this.grid, i);
}
newCell = new (renderer as any)(...args);
}
this.el.appendChild(newCell.el);
this.cellComponentMap[newCell.id] = newCell;
}
for (const cell of removeCells) {
cell.destroy?.(); // Clean up if destroy is available (e.g., unmount React)
delete this.cellComponentMap[cell.id];
this.el.removeChild(cell.el);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment