DEV Community

PlatinumJ
PlatinumJ

Posted on

I vibe coded my own CSV viewer because I didn't like any of the ones that existed

TL;DR

  • Opening a big CSV in VS Code gets you a wall of text, and most grid extensions freeze on large files. So I built OCVYN ("Only CSV Viewer You Need"): a read-only grid for CSV, TSV, XLSX and XLS.
  • It handles CSVs up to 500 MB and Excel up to 50 MB, with Excel-style column filters, multi-level sort and frozen columns. Free, MIT, fully local: no telemetry, no network, the file is never changed.
  • On a 2 vCPU / 2 GB box with a 50 MB CSV: index 0.17 s, filter 0.4 s, multi-column sort 1–1.4 s.
  • The wins came from three things: storing only where each row starts, filtering one column instead of whole rows, and sorting packed numbers instead of calling a comparator.
  • The nastiest bug wasn't speed. Redrawing the grid on every click killed the user's text selection.

OCVYN grid overview

Why

Open a big CSV in VS Code and you get raw text with commas everywhere. Most grid extensions are fine on small files and freeze or choke on anything big.

What I wanted was boring: Excel-style filtering, inside the editor, that stays fast on files of hundreds of MB. That's it. No editing, no charts.

So I vibe coded it with Claude Code. Spec first (speed targets, scope, what's out), then mockups, then the build.

What it does

  • Opens CSV/TSV up to 500 MB, Excel up to 50 MB (with a sheet picker). First rows show almost instantly, the rest loads in the background.
  • Column filter like Excel: a dropdown of every distinct value with counts. Paginated, searchable, tick and untick, select all.
  • LIKE search in the same dropdown. Plain text means "contains". % and _ work like SQL, so pa% or %ing.
  • Rule filters (amount > 500, status is not paid) joined by all/any, plus a global search on Ctrl/⌘+F.
  • Multi-level sort. Numbers sort as numbers, blanks go last.
  • Freeze columns, with a visible frozen edge and a ❄ marker.
  • Select cells, rows (Shift/Ctrl) or whole columns. Right-click a column to sort, filter, freeze or copy.
  • Ctrl/⌘+C copies tab-separated text, so it pastes straight into Excel or Sheets.
  • Colours come from your VS Code theme, light or dark.

Column filter dropdown

How it stays fast

Everything heavy (parsing, filtering, sorting) runs in a Node worker_threads worker. The editor never blocks, even while a 500 MB file is still indexing.

1. Don't parse the file. Remember where rows start.

Parsing every row up front is the obvious move, and it's slow and eats memory. Instead OCVYN makes one pass over the raw buffer and stores only the byte offset where each row starts:

// simplified
const starts = new Uint32Array(estimatedRows)
let row = 0, pos = 0
while (pos < buf.length) {
  starts[row++] = pos
  const nl = buf.indexOf(10, pos)   // '\n'
  if (nl === -1) break
  pos = nl + 1
}
Enter fullscreen mode Exit fullscreen mode

buf.indexOf runs in native code, so this is fast. A row only gets parsed when something actually needs it: the grid wants to draw it, or a filter wants one of its cells. Rows with no quotes take a plain split path. Only rows with quotes go through the full CSV parser.

That's how a 50 MB file indexes in about 0.17 s.

2. Filters read one column, not the whole row

The naive filter is "parse the row, check the cell". On a big file that means splitting every row into every column just to look at one of them.

So each filter reads only its own column for each row. That one change was one of the two biggest wins when I profiled it.

Long filters also pause every 64k rows to stay responsive, and every query has an ID. If you change the filter while an old one is still running, the new query cancels the old one. No piling up.

3. Sort numbers, not objects

A comparator sort (arr.sort((a, b) => ...)) calls your JS function millions of times. That's where multi-column sorts get slow.

OCVYN turns the sort key into an integer rank, then packs rank and original position into one number:

// rank[p] = the sort key's rank for the row at position p
const packed = new Float64Array(n)
for (let p = 0; p < n; p++) packed[p] = rank[p] * n + p

packed.sort()   // typed arrays sort numerically, no callback

for (let i = 0; i < n; i++) order[i] = packed[i] % n
Enter fullscreen mode Exit fullscreen mode

Now the engine's built-in numeric sort does all the work, with zero callbacks. Adding the position also makes the sort stable for free: equal ranks stay in their original order.

It stays exact because a Float64 holds integers up to 2^53. With n rows and ranks below n, the biggest value is about n², and even 5 million rows comes out around 2.5 × 10¹³. Plenty of room.

4. Only draw what's on screen

The grid only renders the visible rows. Rows are fetched from the worker in blocks of 100, with a small cache.

One catch: browsers have a max element height. Millions of rows at normal row height blow way past it. So the scroll height is capped, and scroll position is scaled to the row index. Huge files still scroll end to end.

The bug that ate my text selection

You'd select some text in a cell, click somewhere, and the selection was gone. The reason: every click re-rendered the grid. The browser's text selection is attached to specific DOM nodes. Throw those nodes away and draw new ones, and the selection goes with them.

The fix was to stop redrawing on click. Selection now updates by toggling classes on the existing cells, in place. The DOM stays put, and the browser's selection survives.

Same idea for row hover. I compute it from the mouse position instead of relying on CSS :hover, which kept breaking while scrolling.

Lesson: in a virtual grid, "just re-render" has side effects you don't see in a profiler.

Column right-click menu

Numbers

Measured on a 2 vCPU / 2 GB cloud box, 50 MB CSV:

Operation Time
Index the whole file ~0.17 s
Filter ~0.4 s
Multi-column sort ~1–1.4 s

The 500 MB limit is designed for, but I haven't benchmarked it yet. When I do, the numbers go in a follow-up, good or bad.

Shipping it

Some things I learned getting it ready to publish:

  • Keep "Excel" and "VS Code" out of the extension name. Trademarks. Say what it does in the description instead.
  • Credit bundled libraries. Excel reading uses SheetJS Community Edition (Apache-2.0), credited in THIRD_PARTY_NOTICES.
  • Ship a privacy note even if you collect nothing. "No telemetry, no network calls" written down beats people guessing.
  • Lock it down even with no network. The webview runs under a strict Content Security Policy with a nonce.
  • Marketplace personal access tokens retire on 1 Dec 2026. If you publish with a PAT, plan for that now.
  • Publish to Open VSX too, so Cursor and VSCodium users can install it.

Try it

If you work with big CSVs, try it on your worst file and tell me what breaks. Bug reports with ugly real-world CSVs are lowkey the best gift you can give me.

Top comments (0)