Skip to content

Commit 7bb1677

Browse files
authored
Merge pull request #2 from opensass/bench
feat: add TanStack Table vs Table RS benchmark
2 parents 4f96a72 + 07a12fa commit 7bb1677

33 files changed

+3611
-0
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,35 @@ Refer to [our guide](https://github.com/opensass/table-rs/blob/main/DIOXUS.md) t
5454
<!-- absolute url for docs.rs cause LEPTOS.md is not included in crate -->
5555
Refer to [our guide](https://github.com/opensass/table-rs/blob/main/LEPTOS.md) to integrate this component into your Leptos app.
5656

57+
## 📊 Benchmark: TanStack Table vs Table RS
58+
59+
| Metric | TanStack Table (React) | Table RS (Yew + WASM) |
60+
|--------------------------------|-----------------------------|----------------------------|
61+
| **Page Load Time (1M rows)** | ~10 seconds | ~2 seconds |
62+
| **Memory Heap Usage** | >3 GB (heap overflow) | ~1 MB (stable) |
63+
| **Initial Rendering** | Heavy blocking, slow DOM paint | Efficient, lightweight rendering |
64+
| **Browser Responsiveness** | Delayed interactivity | Smooth after hydration |
65+
| **Sorting Performance** | 2-4s for large columns | Sub-1s due to WASM speed |
66+
| **Search Performance** | Acceptable, but slower | Instantaneous, even at scale |
67+
| **Lighthouse Performance Score** | 49/100 | 60/100 |
68+
| **Scalability** | Limited due to memory and VDOM | Near-native scalability |
69+
70+
### 🟨 TanStack Table (React)
71+
- Uses Virtual DOM and JS heap to manage massive data.
72+
- Runtime bottlenecks emerge with >100k rows.
73+
- Memory allocation during sorting and filtering can spike to **3GB+**, often leading to **heap overflow** during intensive usage.
74+
- Lighthouse audit shows poor TTI and CPU blocking.
75+
76+
### 🟩 Table RS (Yew + WASM)
77+
- WASM-compiled logic is highly memory-efficient and deterministic.
78+
- DOM rendering is direct, bypassing React's reconciliation.
79+
- Only ~1MB of memory heap used even with **1 million rows**.
80+
- Built-in support for search/sort with stable paging.
81+
- No hydration issues (client-only generation).
82+
- Lighthouse performance significantly better, especially in CPU/Memory metrics.
83+
84+
For large-data UI benchmarks like tables with millions of rows, **`table-rs` in Yew/WASM is a superior choice** compared to React + TanStack.
85+
5786
## 🤝 Contributions
5887

5988
Contributions are welcome! Whether it's bug fixes, feature requests, or examples, we would love your help to make **Table RS** even better.

bench/next-js/.gitignore

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts

bench/next-js/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Next JS Bench
2+
3+
```sh
4+
# production build
5+
npm run build
6+
```
7+
8+
```sh
9+
# serve
10+
npm run start
11+
```

bench/next-js/app/favicon.ico

25.3 KB
Binary file not shown.

bench/next-js/app/globals.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@import "tailwindcss";
2+
3+
:root {
4+
--background: #ffffff;
5+
--foreground: #171717;
6+
}
7+
8+
@theme inline {
9+
--color-background: var(--background);
10+
--color-foreground: var(--foreground);
11+
--font-sans: var(--font-geist-sans);
12+
--font-mono: var(--font-geist-mono);
13+
}
14+
15+
@media (prefers-color-scheme: dark) {
16+
:root {
17+
--background: #0a0a0a;
18+
--foreground: #ededed;
19+
}
20+
}
21+
22+
body {
23+
background: var(--background);
24+
color: var(--foreground);
25+
font-family: Arial, Helvetica, sans-serif;
26+
}

bench/next-js/app/layout.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Geist, Geist_Mono } from "next/font/google";
2+
import "./globals.css";
3+
4+
const geistSans = Geist({
5+
variable: "--font-geist-sans",
6+
subsets: ["latin"],
7+
});
8+
9+
const geistMono = Geist_Mono({
10+
variable: "--font-geist-mono",
11+
subsets: ["latin"],
12+
});
13+
14+
export const metadata = {
15+
title: "Create Next App",
16+
description: "Generated by create next app",
17+
};
18+
19+
export default function RootLayout({ children }) {
20+
return (
21+
<html lang="en">
22+
<body
23+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
24+
>
25+
{children}
26+
</body>
27+
</html>
28+
);
29+
}

bench/next-js/app/page.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"use client";
2+
3+
import React, { useMemo, useState, useRef, useEffect } from "react";
4+
import {
5+
useReactTable,
6+
getCoreRowModel,
7+
getSortedRowModel,
8+
getFilteredRowModel,
9+
flexRender,
10+
} from "@tanstack/react-table";
11+
12+
import { useVirtualizer } from "@tanstack/react-virtual";
13+
14+
const generateData = (count = 1_000_000) => {
15+
return Array.from({ length: count }, (_, i) => ({
16+
id: i + 1,
17+
name: `User ${i + 1}`,
18+
email: `user${i + 1}@example.com`,
19+
age: Math.floor(Math.random() * 80) + 18,
20+
registered: new Date(Date.now() - Math.random() * 1e12).toISOString(),
21+
}));
22+
};
23+
24+
const Home = () => {
25+
const [data, setData] = useState([]);
26+
27+
useEffect(() => {
28+
performance.mark("data-gen-start");
29+
const generated = generateData();
30+
setData(generated);
31+
performance.mark("data-gen-end");
32+
performance.measure("Data Generation", "data-gen-start", "data-gen-end");
33+
}, []);
34+
35+
const [sorting, setSorting] = useState([]);
36+
const [globalFilter, setGlobalFilter] = useState("");
37+
38+
const rerenderCount = useRef(0);
39+
rerenderCount.current++;
40+
41+
const columns = useMemo(
42+
() => [
43+
{
44+
accessorKey: "id",
45+
header: "ID",
46+
},
47+
{
48+
accessorKey: "name",
49+
header: "Name",
50+
},
51+
{
52+
accessorKey: "email",
53+
header: "Email",
54+
},
55+
{
56+
accessorKey: "age",
57+
header: "Age",
58+
},
59+
{
60+
accessorKey: "registered",
61+
header: "Registered",
62+
},
63+
],
64+
[],
65+
);
66+
67+
const table = useReactTable({
68+
data,
69+
columns,
70+
state: {
71+
sorting,
72+
globalFilter,
73+
},
74+
onSortingChange: (updater) => {
75+
performance.mark("sort-start");
76+
setSorting(updater);
77+
},
78+
getCoreRowModel: getCoreRowModel(),
79+
getSortedRowModel: getSortedRowModel(),
80+
getFilteredRowModel: getFilteredRowModel(),
81+
});
82+
83+
const parentRef = useRef(null);
84+
const rowVirtualizer = useVirtualizer({
85+
count: table.getRowModel().rows.length,
86+
getScrollElement: () => parentRef.current,
87+
estimateSize: () => 35,
88+
overscan: 20,
89+
});
90+
91+
useEffect(() => {
92+
performance.mark("render-end");
93+
94+
try {
95+
performance.measure("Sort Duration", "sort-start", "render-end");
96+
performance.measure("Search Duration", "search-start", "render-end");
97+
} catch (e) {
98+
}
99+
});
100+
101+
const handleSearchChange = (e) => {
102+
performance.mark("search-start");
103+
setGlobalFilter(e.target.value);
104+
};
105+
if (data.length === 0) {
106+
return <div>Loading table data...</div>;
107+
}
108+
109+
return (
110+
<div>
111+
<h1>TanStack Table Benchmark (1M rows)</h1>
112+
<p>Re-renders: {rerenderCount.current}</p>
113+
<input
114+
type="text"
115+
placeholder="Search..."
116+
value={globalFilter}
117+
onChange={handleSearchChange}
118+
style={{ marginBottom: "10px", padding: "6px", width: "300px" }}
119+
/>
120+
121+
<div
122+
ref={parentRef}
123+
style={{
124+
height: "600px",
125+
overflow: "auto",
126+
border: "1px solid #ccc",
127+
}}
128+
>
129+
<table style={{ width: "100%", borderCollapse: "collapse" }}>
130+
<thead>
131+
{table.getHeaderGroups().map((headerGroup) => (
132+
<tr key={headerGroup.id}>
133+
{headerGroup.headers.map((header) => (
134+
<th
135+
key={header.id}
136+
onClick={header.column.getToggleSortingHandler()}
137+
style={{
138+
cursor: "pointer",
139+
borderBottom: "1px solid black",
140+
background: "#f0f0f0",
141+
}}
142+
>
143+
{flexRender(
144+
header.column.columnDef.header,
145+
header.getContext(),
146+
)}
147+
{header.column.getIsSorted()
148+
? header.column.getIsSorted() === "asc"
149+
? " 🔼"
150+
: " 🔽"
151+
: ""}
152+
</th>
153+
))}
154+
</tr>
155+
))}
156+
</thead>
157+
<tbody style={{ position: "relative" }}>
158+
<tr style={{ height: `${rowVirtualizer.getTotalSize()}px` }} />
159+
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
160+
const row = table.getRowModel().rows[virtualRow.index];
161+
return (
162+
<tr
163+
key={row.id}
164+
style={{
165+
position: "absolute",
166+
top: 0,
167+
transform: `translateY(${virtualRow.start}px)`,
168+
height: "35px",
169+
}}
170+
>
171+
{row.getVisibleCells().map((cell) => (
172+
<td
173+
key={cell.id}
174+
style={{ padding: "4px", borderBottom: "1px solid #eee" }}
175+
>
176+
{flexRender(
177+
cell.column.columnDef.cell,
178+
cell.getContext(),
179+
)}
180+
</td>
181+
))}
182+
</tr>
183+
);
184+
})}
185+
</tbody>
186+
</table>
187+
</div>
188+
</div>
189+
);
190+
};
191+
192+
export default Home;

bench/next-js/jsconfig.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"paths": {
4+
"@/*": ["./*"]
5+
}
6+
}
7+
}

bench/next-js/next.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {};
3+
4+
export default nextConfig;

0 commit comments

Comments
 (0)