Svelte Trading Grid
A blotter is a grid sorted by something that ticks. Last price, percent change, P&L: the column the desk watches is the column the feed moves, so every update has to land in sorted position and the grid has to do that at the row count a desk actually has, not a demo's dozen.
SvGrid takes a tick feed on one path: the messages wait in a map keyed by instrument, the newest price per id wins the frame, and one applyTransaction per animation frame hands the grid a new array in which only the ticked rows are new objects. The row model reuses every row that did not change and repairs the sort around the ones that did instead of re-sorting 100,000 rows; the cost of that tick is measured and published on the benchmarks page, with the harness that produced it in the repository, next to the number a full sort would have cost.
The rest of the blotter is the grid as it comes: cellFlash restarts a colour fade only when that row's value changes, number formats with negative sections and a currency, sparklines from a rolling buffer, pinned symbol, row virtualization so 100,000 rows keep a few dozen elements in the DOM. For books larger than a browser tab should hold, the Enterprise server row model streams the visible window from your service and applies transactions to it.
Install
npm i @svgrid/gridFree and MIT-licensed in @svgrid/grid: no license key, no row cap, no watermark.
The code
<script lang="ts">
import { SvGrid, tableFeatures, rowSortingFeature, type GridColumns, type SvGridApi } from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature })
let rows = $state.raw<Quote[]>(initialQuotes)
let api = $state<SvGridApi | null>(null)
// Ticks wait in a map (newest price per id wins the frame) and go through
// one applyTransaction per animation frame: one data change, one pipeline
// run, however many messages arrived.
const pending = new Map<string, number>()
let frame: number | null = null
socket.onmessage = (e) => {
for (const [id, last] of JSON.parse(e.data).t) pending.set(id, last)
if (frame === null) frame = requestAnimationFrame(flush)
}
function flush() {
frame = null
const byId = new Map(api!.getData().map((r) => [r.id, r]))
const update = [...pending].flatMap(([id, last]) => {
const row = byId.get(id)
return row ? [{ ...row, last, direction: last > row.last ? 'up' : 'down' }] : []
})
pending.clear()
api!.applyTransaction({ update })
}
const columns: GridColumns<Quote> = [
{ field: 'symbol', header: 'Symbol' },
{ field: 'last', header: 'Last', editorType: 'number',
format: { type: 'number', options: { minimumFractionDigits: 2, maximumFractionDigits: 2 } },
cellFlash: { className: 'tick' },
cellClass: (ctx) => (ctx.row.original.direction === 'up' ? 'tick-up' : 'tick-down') },
{ field: 'pct', header: 'Chg %', editorType: 'number' },
]
</script>
<SvGrid data={rows} {columns} {features} getRowId={(r) => r.id}
initialSorting={[{ id: 'pct', desc: true }]} sortable onApiReady={(a) => (api = a)} />What you get
- Stays sorted under the feed - A tick of replaced rows on a sorted grid repairs the previous order around the rows that changed. Same result as a full sort, a fraction of the cost, and a test that compares the two on every kind of sort key.
- One transaction per frame - applyTransaction({ update }) matches rows by id through a map built once per array, so a batch of a thousand updates is a thousand lookups and one copy, not a pass over every row.
- Cell flash that knows the row - cellFlash is keyed by row identity: a price change flashes, a row scrolling into a recycled cell does not. Pass your own class for green up, red down.
- Finance formats - Number formats with positive, negative and zero sections, [Red] negatives, accounting parentheses, currencies and Excel pattern strings, all on the raw number so sorting stays numeric.
- Sparklines and pinned columns - A sparkline column type over a rolling buffer per row, symbol pinned left, P&L pinned right, column virtualization for the wide books.
- A million rows from the server - The Enterprise server row model streams the visible window from your service, supports select-all across rows the browser never loaded, and applies transactions to what is loaded.
Live examples
- Market blotter: 100,000 rows on a socket - 100,000 instruments sorted by % change while a WebSocket feed pushes 1k, 10k or 50k price updates a second. Ticks wait in a map for the next animation frame and go through one applyTransaction per frame; the grid repairs the sort around the rows that changed instead of re-sorting 100k, and cellFlash colours each move. Frame-time p95 on screen. Point it at node tools/tick-server.mjs with ?ws=.
- Trading desk - live - 10,000 securities ticking on a 500 ms feed. Pinned Symbol + P&L, per-company logo marks, direction-coloured sparklines, sector chips, a KPI strip, and a notifications bell that flags standout movers. The hero.
- Stock market - live - WebSocket-style ticking feed. Cells flash on up/down ticks, pause control, throttle.
- Transaction API (batched) - api.applyTransaction({ add, update, remove }) applies a batch of row mutations in ONE data update - the high-frequency streaming path. update and remove-by-id match on getRowId; remove also accepts row refs. Live order book ticking via batched transactions.
- Real-time / streaming - WebSocket-style live order stream with delta merge, out-of-order safety, pause / backlog, disconnect-reconnect, throughput slider.
- Server transactions (live feed) - A socket-style feed of changes the server already made, applied without a refetch: a price tick patches the loaded row in place with a flash (updateRowData), a new order lands at the top of its warehouse and a shipped one leaves (applyTransactionAsync, batched every 500 ms, addressed by route). Every result carries a status the log shows: applied, cancelled under the veto hook, storeNotFound for a warehouse whose level is not cached. Refresh totals recomputes the sums a transaction leaves alone.
Documentation
- Real-time / streaming updates - How to drive the grid from a WebSocket / SSE / poll. Four patterns ranked by the rate of change:
- Transactions - api.applyTransaction({ add, update, remove }) applies a batch of row mutations in a single data update - one re-render for the whole batch, not one per row.…
- Highlighting changes - Set cellFlash on a column and the grid flashes that cell whenever its value changes - edits, streaming feeds, server pushes. It is keyed by row identity, so…
- Number formats and cell styles - In Excel, format is a property of the cell, not the column. Two cells in the same column can show $1,234.50 and 123450% from the same stored number.…
- Performance benchmarks - Every number on this page is produced by a checked-in script:
- Server transactions - Enterprise - A row was added on the server and the grid should show it now, not after a refetch. A socket said a price moved. A row was deleted and its group should lose…
Related articles
- Building a Real-Time Trading Grid in Svelte - How to wire a WebSocket tick feed into SvGrid without dropping frames - one array replacement per animation frame, stable row identity, up/down flashes, and which of the grid's two update paths to use for what.
- Real-Time Grids - Live WebSocket Updates in SvGrid - How to wire a high-frequency WebSocket feed into SvGrid without frame drops - covering stable row identity, RAF batching, and flash feedback done right.
- What Makes a Svelte Data Grid Fast (and How to Measure It) - Performance claims are easy to make. Here is how to actually measure grid speed - what metrics matter, what traps to avoid, and what the fundamentals look like in code.
Frequently asked questions
How many rows can a Svelte data grid keep sorted while prices tick?
The market blotter demo keeps 100,000 rows sorted by percent change with the feed pushing tens of thousands of updates a second, batched per animation frame. The engine cost of a tick, and how it compares to a full sort, is measured on the benchmarks page with the script that produced it.
Should I mutate rows in place or replace them?
For a sorted blotter, replace them: a proxy write updates one cell but the row model never runs, so the sort goes stale. applyTransaction replaces the ticked rows in a new array and the grid repairs the order around them. Mutate in place only when you will refresh with a new array afterwards.
Why batch ticks per animation frame?
The screen paints once per frame, so applying ticks faster than that runs the pipeline for results nobody sees. A map keyed by instrument merges a symbol that ticked five times in a frame into one update with its latest price.
Can I connect it to my own WebSocket feed?
Yes. The blotter demo reads any WebSocket URL you pass, and tools/tick-server.mjs in the repository is a forty-line feed with no dependency that shows the message format, so it doubles as a template for adapting yours.
Is the benchmark comparable to other grids?
The comparison harness in the repository runs the same tick against several grids, on each grid's own update path, and checks after the ticks that every grid kept its sort; a grid that answered a data change by dropping the order is marked, not credited. The results and the method are on the comparison page.