Skip to content

perf(#230): remove O(N^2) per-cell row-index lookup in the data grid#235

Open
husamql3 wants to merge 1 commit into
stagefrom
perf/#230-table-cell-rowindex
Open

perf(#230): remove O(N^2) per-cell row-index lookup in the data grid#235
husamql3 wants to merge 1 commit into
stagefrom
perf/#230-table-cell-rowindex

Conversation

@husamql3

@husamql3 husamql3 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Memoized the display-row-index computation in TableCell — the component ran rows.findIndex(...) (a linear scan of the whole row model) on every cell render, so a full grid render was O(rows² × cols) and re-ran on every focus/edit/selection toggle. The scan is now wrapped in useMemo keyed on the row-model identity and this row's data, so it recomputes only when an input that could change the result changes.
  • Semantics unchangedrowIndex still resolves to the same value (display position within the current, possibly sorted, row model), so focus/edit/selection targeting is identical; only the redundant per-render recomputation is removed.

Testing

  1. bun run typecheck → passes
  2. bun run format → passes
  3. bun run build → passes

Closes #230

Glossary

Term Definition
Row model TanStack Table's post-sort/filter ordered rows (getRowModel().rows), which can differ from pre-sort data order.
useMemo React hook that caches a computed value and recomputes only when its dependency list changes.

Summary by CodeRabbit

  • Performance
    • Improved table rendering efficiency by reducing repeated row lookups during cell display.
    • Kept row numbering behavior consistent, including fallback handling when a matching row isn’t found.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The row index computation in TableCell was changed from an unmemoized rows.findIndex scan to a useMemo-wrapped calculation, preserving the same fallback logic to cell.row.index when the row is not found.

Changes

Row Index Memoization

Layer / File(s) Summary
Memoize row-index lookup
packages/web/src/features/tables/components/table-cell.tsx
Adds useMemo import and wraps the previous rows.findIndex-based row-index computation in a memoized calculation keyed on the row model and cell identity, keeping the same fallback behavior.

Estimated code review effort: 1 (Trivial) | ~3 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: removing the per-cell row-index lookup hot path in the data grid.
Linked Issues check ✅ Passed The change memoizes the row-index lookup and preserves the same rowIndex fallback semantics, aligning with #230.
Out of Scope Changes check ✅ Passed The PR only updates table-cell.tsx for the row-index performance fix, with no unrelated changes shown.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/#230-table-cell-rowindex

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bundle Size

packages/server/dist788K

File breakdown
172K	packages/server/dist/index.js
612K	packages/server/dist/index.js.map

packages/web/dist64M

File breakdown
4.0K	packages/web/dist/assets/_pathlessLayout-BULBsQ9r.js
4.0K	packages/web/dist/assets/_pathlessLayout-BULBsQ9r.js.map
56K	packages/web/dist/assets/_pathlessLayout-CdwsO4L4.js
1.2M	packages/web/dist/assets/_pathlessLayout-CdwsO4L4.js.map
4.0K	packages/web/dist/assets/_queryId-BMeUtMCY.js
4.0K	packages/web/dist/assets/_queryId-BMeUtMCY.js.map
4.0K	packages/web/dist/assets/_table-Cito83x4.js
4.0K	packages/web/dist/assets/_table-Cito83x4.js.map
4.0K	packages/web/dist/assets/_table-CwdYj8bn.js
4.0K	packages/web/dist/assets/_table-CwdYj8bn.js.map
28K	packages/web/dist/assets/bundle-mjs-BC_8HrWA.js
140K	packages/web/dist/assets/bundle-mjs-BC_8HrWA.js.map
444K	packages/web/dist/assets/chunk-BO2N2NFS-D1cKzRy7.js
1.7M	packages/web/dist/assets/chunk-BO2N2NFS-D1cKzRy7.js.map
8.0K	packages/web/dist/assets/code-editor-D1LiAEIU.js
20K	packages/web/dist/assets/code-editor-D1LiAEIU.js.map
120K	packages/web/dist/assets/codicon-ngg6Pgfi.ttf
1.3M	packages/web/dist/assets/css.worker-A3810hic.js
3.2M	packages/web/dist/assets/css.worker-A3810hic.js.map
36K	packages/web/dist/assets/dist-m8A6NETj.js
88K	packages/web/dist/assets/dist-m8A6NETj.js.map
56K	packages/web/dist/assets/format-cell-value-BDLw73u3.js
232K	packages/web/dist/assets/format-cell-value-BDLw73u3.js.map
4.0K	packages/web/dist/assets/highlighted-body-OFNGDK62-NogKB3ej.js
4.0K	packages/web/dist/assets/highlighted-body-OFNGDK62-NogKB3ej.js.map
860K	packages/web/dist/assets/html.worker-DUbSWKmN.js
2.2M	packages/web/dist/assets/html.worker-DUbSWKmN.js.map
20K	packages/web/dist/assets/icons-B5bSDd0r.js
88K	packages/web/dist/assets/icons-B5bSDd0r.js.map
124K	packages/web/dist/assets/index-B-VrElCM.css
460K	packages/web/dist/assets/index-B1PPYukQ.js
1.9M	packages/web/dist/assets/index-B1PPYukQ.js.map
4.0K	packages/web/dist/assets/indexes-CBb_pYf_.js
4.0K	packages/web/dist/assets/indexes-CBb_pYf_.js.map
12K	packages/web/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2
12K	packages/web/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2
16K	packages/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2
40K	packages/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2
8.0K	packages/web/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2
516K	packages/web/dist/assets/json.worker-qcFW0tBA.js
1.7M	packages/web/dist/assets/json.worker-qcFW0tBA.js.map
4.0K	packages/web/dist/assets/logs-BDeicfXz.js
4.0K	packages/web/dist/assets/logs-BDeicfXz.js.map
4.0K	packages/web/dist/assets/mermaid-GHXKKRXX-kUDCGRaV.js
4.1M	packages/web/dist/assets/monaco-editor-DJLWokZr.js
15M	packages/web/dist/assets/monaco-editor-DJLWokZr.js.map
140K	packages/web/dist/assets/monaco-editor-DVSAlpqy.css
200K	packages/web/dist/assets/monitoring-Dw3KwoPJ.js
512K	packages/web/dist/assets/monitoring-Dw3KwoPJ.js.map
4.0K	packages/web/dist/assets/queries.store-DiPSGSR2.js
8.0K	packages/web/dist/assets/queries.store-DiPSGSR2.js.map
12K	packages/web/dist/assets/query-runner-Dr3ScR6w.js
32K	packages/web/dist/assets/query-runner-Dr3ScR6w.js.map
148K	packages/web/dist/assets/radix-ui-DS4zDRqg.js
672K	packages/web/dist/assets/radix-ui-DS4zDRqg.js.map
176K	packages/web/dist/assets/react-dom-DafrnciU.js
784K	packages/web/dist/assets/react-dom-DafrnciU.js.map
4.0K	packages/web/dist/assets/rolldown-runtime-b3L32Ng1.js
4.0K	packages/web/dist/assets/runner-B5lMaDAW.js
4.0K	packages/web/dist/assets/runner-B5lMaDAW.js.map
4.0K	packages/web/dist/assets/schema-4dGwYfjY.js
4.0K	packages/web/dist/assets/schema-4dGwYfjY.js.map
4.0K	packages/web/dist/assets/table-voW5jQda.js
4.0K	packages/web/dist/assets/table-voW5jQda.js.map
276K	packages/web/dist/assets/tanstack-BfO3BbGk.js
1.1M	packages/web/dist/assets/tanstack-BfO3BbGk.js.map
7.8M	packages/web/dist/assets/ts.worker-BmSK7P1D.js
17M	packages/web/dist/assets/ts.worker-BmSK7P1D.js.map
4.0K	packages/web/dist/assets/visualizer-s3AQxGwZ.js
4.0K	packages/web/dist/assets/visualizer-s3AQxGwZ.js.map
104K	packages/web/dist/image.png
4.0K	packages/web/dist/index.html

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/features/tables/components/table-cell.tsx`:
- Around line 22-30: The display-index lookup in TableCell is still happening
per cell via useMemo, so every row-model update re-runs rows.findIndex for each
visible cell and keeps rendering quadratic. Move this row-position calculation
out of TableCell into a shared parent or row renderer that already iterates the
table rows, and pass the computed display index down to TableCell using the
existing row/cell rendering path so it is computed once per row-model change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8e39cf95-f553-494f-a503-ba3c714c8cdf

📥 Commits

Reviewing files that changed from the base of the PR and between b86b1de and fc5879f.

📒 Files selected for processing (1)
  • packages/web/src/features/tables/components/table-cell.tsx

Comment on lines 22 to +30

// Display position within the current (possibly sorted/filtered) row model.
// Memoized on the row-model identity + this row's data so the O(N) scan runs
// at most once per render pass per row model rather than on every cell render.
const rows = table.getRowModel().rows;
const displayRowIndex = rows.findIndex((row) => row.original === cell.row.original);
const rowIndex = displayRowIndex >= 0 ? displayRowIndex : originalRowIndex;
const rowIndex = useMemo(() => {
const displayRowIndex = rows.findIndex((row) => row.original === cell.row.original);
return displayRowIndex >= 0 ? displayRowIndex : cell.row.index;
}, [rows, cell.row.original, cell.row.index]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file first, then inspect the smallest useful slices.
ast-grep outline packages/web/src/features/tables/components/table-cell.tsx --view expanded || true
wc -l packages/web/src/features/tables/components/table-cell.tsx

echo
echo "---- table-cell.tsx (selected lines) ----"
sed -n '1,220p' packages/web/src/features/tables/components/table-cell.tsx

echo
echo "---- search for row rendering / table cell usage ----"
rg -n "TableCell|table.getRowModel\(\)\.rows|findIndex\(|row.original|row.index" packages/web/src/features/tables -S

Repository: husamql3/db-studio

Length of output: 8556


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- table-body-row.tsx ----"
wc -l packages/web/src/features/tables/components/table-body-row.tsx
sed -n '1,220p' packages/web/src/features/tables/components/table-body-row.tsx

echo
echo "---- table-body.tsx ----"
wc -l packages/web/src/features/tables/components/table-body.tsx
sed -n '1,220p' packages/web/src/features/tables/components/table-body.tsx

echo
echo "---- use-table-model.ts (relevant slice) ----"
wc -l packages/web/src/features/tables/hooks/use-table-model.ts
sed -n '1,220p' packages/web/src/features/tables/hooks/use-table-model.ts

Repository: husamql3/db-studio

Length of output: 8541


Hoist the display-index lookup out of TableCell. useMemo is still per cell, so every row-model change re-runs rows.findIndex(...) in every visible cell. That keeps the render pass at O(rows² × cols). Compute the index once per row-model change in a shared parent, or pass the row position down from the row renderer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/features/tables/components/table-cell.tsx` around lines 22 -
30, The display-index lookup in TableCell is still happening per cell via
useMemo, so every row-model update re-runs rows.findIndex for each visible cell
and keeps rendering quadratic. Move this row-position calculation out of
TableCell into a shared parent or row renderer that already iterates the table
rows, and pass the computed display index down to TableCell using the existing
row/cell rendering path so it is computed once per row-model change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(web): remove O(N^2) per-cell row-index lookup in the data grid

1 participant