Skip to content

feat: virtual scroll, fuzzy search, default columns, inline data#1041

Open
sunipan wants to merge 19 commits intoanomalyco:devfrom
sunipan:feat/perf-improvements
Open

feat: virtual scroll, fuzzy search, default columns, inline data#1041
sunipan wants to merge 19 commits intoanomalyco:devfrom
sunipan:feat/perf-improvements

Conversation

@sunipan
Copy link

@sunipan sunipan commented Feb 25, 2026

Closes #977

Builds on the approach from #1016 by @gianpaj (TanStack Table + virtual scroll). This PR adds search overhaul, sensible defaults, and inline data to eliminate the loading flash.

Summary

  • Virtual scroll (from feat: Tanstack Table + virtual scroll with column picker and URL state #1016): TanStack Table + Virtual — only ~35 DOM rows rendered at a time instead of 3,039
  • Column picker (from feat: Tanstack Table + virtual scroll with column picker and URL state #1016): grouped checkbox dropdown to show/hide any of the 25 columns
  • Fuzzy search: MiniSearch replaces brute-force String.includes() — supports typo tolerance ("claud" → "claude"), prefix matching, and relevance scoring
  • Scoped search fields: indexes only provider name, model name, model ID, provider ID, and family — does NOT match cost values or booleans (fixes web: Select columns to display and improve web performance #977)
  • Debounced input: 150ms debounce on search to avoid per-keystroke re-renders
  • Default columns: 9 curated columns visible by default (Provider, Model, Family, Model ID, Tool Call, Reasoning, Input Cost, Output Cost, Context Limit) instead of all 25
  • localStorage persistence: column visibility preferences survive page reloads
  • Inline JSON data: api.json embedded in HTML at build time — no fetch round-trip, no "Loading models…" flash
  • Mobile-friendly header: search bar visible on mobile as a full-width second row (logo, columns, github, how-to-use on first row); tagline hidden on small screens
  • 41 tests (up from 0): search indexing, fuzzy matching, field exclusion, debounce, column defaults, localStorage precedence

Check it out live at: https://models-dev.pages.dev

Performance

Metric Before After
HTML size (gzip) 373 KB 110 KB
DOM nodes at load ~200,000+ ~750
Search Brute-force includes() on all fields MiniSearch fuzzy on text fields only
Default columns All 25 (horizontal scroll) 9 curated
Data loading Baked in 12MB HTML Inline JSON (110KB gzip)

Files changed

Only packages/web/ modified — no changes to providers/, packages/core/, or packages/function/.

New dependencies: @tanstack/table-core, @tanstack/virtual-core, minisearch

Validation

  • bun validate
  • bun test ✅ (41 pass, 0 fail)
  • bun run build

Credit

Foundation (TanStack Table + Virtual, column picker, URL state) by @gianpaj in #1016.

gianpaj and others added 15 commits February 24, 2026 07:05
Replaces imperative sort/filter/render with declarative Tanstack Table
(createTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel)
and virtual row rendering via Virtualizer class. Adds column picker
dropdown, URL state sync (search/sort/order/cols), and keyboard
shortcut Cmd+K to focus search.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n-id to tds

- Add #table-scroll-container with full-viewport scroll height
- Make thead th sticky at top:0 within scroll container
- Replace nth-child column selectors with data-column-id attribute selectors
- Add column picker dropdown styles (.picker-group, .picker-item, etc.)
- Add .provider-logo img style; remove stale .provider-cell svg rule
- Add #table-loading style
- Add data-column-id to each td in renderRows()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In vanilla JS, Virtualizer._willUpdate() must be called manually to
initialize scroll element observation (frameworks call it automatically
via lifecycle hooks like useLayoutEffect). Without it, scrollElement
stays null, observers are never set up, and getVirtualItems() always
returns an empty array.

Also added /api.json route to the dev server so the client-side fetch
succeeds in development mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Embed api.json directly into HTML as <script type="application/json">
at build time, eliminating the fetch round-trip and loading flash.

- Add model-data script placeholder to index.html
- Inject JSON data in build script after static HTML replacement
- Remove loading div from render.tsx (data available immediately)
- Update dev server to also inject JSON for local development
- Keep _api.json output for external API consumers

HTML: 4.7KB → 1.2MB raw / 110KB gzipped (vs original 12MB / 373KB)
Show 9 curated columns by default instead of all 25:
Provider, Model, Family, Model ID, Tool Call, Reasoning,
Input Cost, Output Cost, Context Limit.

- Export DEFAULT_COLUMN_IDS from url-state.ts
- parseUrlState() returns defaults when no cols param in URL
- serializeUrlState() omits cols when matching defaults
- Update existing tests and add new ones for column defaults
Replace brute-force String.includes() search with MiniSearch for fuzzy
matching, prefix search, and relevance scoring. Fixes issue anomalyco#977 where
searching matched cost values.

- New search.ts module with buildSearchIndex() and searchRows()
- Index only text fields: provider name, model name, model ID, family
- Does NOT index cost values, booleans, or limits
- Fuzzy matching: "claud" finds "claude", "gpt4" finds "gpt-4"
- Comma-separated multi-term OR logic preserved
- Add debounce utility with cancel() for Escape key handling
- Add minisearch ^7 dependency
Wire together MiniSearch, debounce, localStorage column persistence,
and inline JSON data loading in the main client entry point.

- Replace TanStack globalFilterFn with MiniSearch pre-filtering
- Remove getFilteredRowModel() (pre-filter rows before passing to TanStack)
- Add 150ms debounced search input handler
- Escape key cancels debounce and clears immediately
- localStorage persistence for column visibility (models.dev:cols)
- Priority: URL cols param > localStorage > DEFAULT_COLUMN_IDS
- init() now synchronous (reads inline JSON, no fetch)
- Build search index once on data load
Add 27 new tests (41 total, up from 14):

- search.test.ts (14): index building, fuzzy matching, prefix search,
  field exclusion (costs/booleans not indexed), comma-separated OR
- debounce.test.ts (4): delay timing, call coalescing, cancel behavior
- columns.test.ts (9): DEFAULT_COLUMN_IDS validation, parseUrlState
  column override, invalid ID filtering
@gianpaj
Copy link

gianpaj commented Feb 25, 2026

Screenshot_20260225-204002

Looks great! The only thing is that the search input is not visible on mobile, I think it should

@sunipan sunipan force-pushed the feat/perf-improvements branch from 3c655bf to 3df9ad9 Compare February 25, 2026 20:53
sunipan and others added 4 commits February 25, 2026 13:24
Compute per-column widths from rendered data so full cell text remains visible, including model IDs with persistent copy controls and a consistently visible copy button.
@sunipan sunipan force-pushed the feat/perf-improvements branch from ca33147 to 86cf580 Compare February 26, 2026 01:38
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.

web: Select columns to display and improve web performance

2 participants