spindle.by jsj
spindle/table

Benchmarks

Rendering speed with virtualization, and a side-by-side with Filament.

Rendering

Measured on 2026-09-29: the prebuilt <Table> in Chromium, production build, no server (requests finish at once). The table mirrors the demo’s projects benchmark table: ten columns with a description, two badges, money, a tag list, a toggle and a date, plus a checkbox, two row actions and a record link per row (about 70 DOM elements per row before this work). Each number is the time from the change until the next frame after it (script, style, layout and paint), the median of 5 runs on a fresh page (11 for the 25- and 100-row first renders, which vary most). Anything near 17 ms is a single frame, the floor of this method.

“Before” is the table without virtualization and the per-row savings; “after” virtualizes above 60 rows (so the 25-row table renders every row in both).

Desktop (Apple M4) 25 rows 100 rows 1,000 rows 5,000 rows
First render 57 → 52 114 → 62 986 → 77 4,008 → 85
Refresh (new payload, same rows) 12 → 5 21 → 10 178 → 16 1,103 → 9
Select all 18 → 15 20 → 14 124 → 15 1,157 → 13
Select one row 15 → 17 17 → 17 85 → 16 681 → 15
Client-side search, one keystroke 17 → 13 18 → 22 81 → 47 727 → 49
Scroll 600 px, worst of 10 steps 17 → 17 19 → 21 22 → 18 61 → 19
DOM elements 1,937 → 1,587 7,337 → 2,459 72,137 → 2,459 360,137 → 2,459
4× CPU slowdown 25 rows 100 rows 1,000 rows 5,000 rows
First render 234 → 212 474 → 259 3,292 → 281 15,633 → 294
Refresh (new payload, same rows) 26 → 19 74 → 26 678 → 31 4,676 → 33
Select all 40 → 35 81 → 42 587 → 38 – → 40
Select one row 37 → 31 73 → 36 408 → 38 – → 44
Client-side search, one keystroke 24 → 26 51 → 49 426 → 115 – → 140
Scroll 600 px, worst of 10 steps 18 → 18 23 → 36 36 → 28 – → 28

(The 4× baseline was not measured at 5,000 rows, where a first render took 15.6 s.)

What the numbers mean:

  • Cost no longer grows with the number of rows. With virtualization, 1,000 or 5,000 loaded rows cost about the same as 100: the DOM holds roughly the rows on screen plus one screen above and below (about 2,500 elements here, against 360,000 for 5,000 rows before).
  • Small tables got faster too. Plain text and badge cells skip the cell component, class strings are merged once per column, selection checks are constant-time and toggles are native buttons, so a 25-row table has 18% fewer elements and renders and updates somewhat faster. Those savings apply to every row that is rendered, virtualized or not.
  • Scrolling now renders rows. The rows scrolling into view are created on the fly, a few per frame. On desktop the worst step stays within a frame or so at any size. At 4× slowdown a 100-row table scrolls a little less smoothly than when all 100 rows were in the DOM (36 against 23 ms for the worst step), in exchange for a first render twice as fast; from 1,000 rows scrolling is smoother than before.
  • Client-side search still filters every loaded row in JavaScript on each keystroke, which is most of what remains of its cost at 1,000 rows and more; rendering the result is now cheap.

Spindle Table vs Filament

Measured on 2026-09-29 with twin tables in the demo app: the same columns, search, sorting, filters, totals, bulk action and 25-row pages, built once with Spindle Table and once with Filament.

  • Invoices: 60,000 rows with company and project relationships, a status badge, money and dates, and a sum of amounts.
  • Projects: 2,500 rows with three relationships, two badges, a tag list, a toggle column and a budget sum.

Numbers are medians of 15 runs after 2 warm-up runs, in milliseconds. The first run rendered Spindle’s pages in the browser only; a second run used Inertia SSR (see With Inertia SSR).

Summary (first run, without SSR)

Desktop (Apple M4) Invoices: Spindle Invoices: Filament Projects: Spindle Projects: Filament
First load: rows on screen (cold cache) 106 47 116 48
First load: largest contentful paint 120 116 128 132
Sort by a column 52 72 50 75
Go to page 2 42 58 47 71
Search, from request start 95 105 71 106
Search, from keystroke (debounce 250 vs 500 ms) 352 616 330 616
Show 100 rows per page 86 136 110 183
Toggle a cell until saved – – 21 40
Select all rows on the page (no request) 8 1 11 11
Response size per interaction, gzip 2.7–5.8 KB 15–33 KB 0.7–7.4 KB 21–52 KB
Response size per interaction, uncompressed 29–101 KB 404–1,370 KB 2–123 KB 577–1,994 KB
4× CPU slowdown (a mid-range laptop or phone) Invoices: Spindle Invoices: Filament Projects: Spindle Projects: Filament
First load: rows on screen (cold cache) 313 78 339 79
First load: largest contentful paint 336 368 364 436
Sort by a column 95 145 134 183
Go to page 2 85 123 107 165
Search, from request start 123 187 125 204
Show 100 rows per page 185 348 266 542
Toggle a cell until saved – – 32 45
Select all rows on the page 34 4 43 7

The server does about the same amount of work in both. A page load takes 27–32 ms on the server in each. Spindle runs 8 queries per table request against Filament’s 10, and peaks 1–2 MB lower in memory. Saving a toggle is the exception: 17 ms against 32–34 ms, because Spindle fetches only the edited row instead of re-rendering the component.

With Inertia SSR

The second run used the demo’s Inertia SSR setup (npm run build:ssr, php artisan inertia:start-ssr), so Spindle’s first HTML already contains the rows. Everything else was unchanged. This run was noisier for both libraries (wider p90s, and Filament’s own interaction times 10–65% slower than in the first run), so compare the two libraries within a run, not across runs.

First load Invoices: Spindle Invoices: Filament Projects: Spindle Projects: Filament
Rows on screen, desktop (cold cache) 48 50 53 68
Largest contentful paint, desktop 72 124 76 172
Rows on screen, 4× slowdown 91 109 102 130
Largest contentful paint, 4× slowdown 164 472 188 660
Server time (includes the SSR render) 37 35 40 44
HTML, gzip 13.8 KB 21.3 KB 17.2 KB 27.4 KB

With SSR, Spindle’s rows appear as soon as Filament’s or sooner, and its largest paint comes 40–70% earlier. The server spends about 10 ms more per page load on the SSR render (37 ms against 27 ms in the first run), and the HTML grows from 4 KB to 14–17 KB gzipped, still smaller than Filament’s. Table interactions are unaffected: they don’t go through SSR, and Spindle stayed 25–50% faster in this run too (for example sort 55 vs 79 ms, 100 rows 95 vs 145 ms on the invoices table).

What the numbers mean

  • Table interactions are 25–50% faster in Spindle, and up to 2× faster with 100 rows or a slower CPU. Spindle sends only the table data as JSON (3–7 KB gzipped) and Svelte updates the rows that changed. Filament re-renders the whole Livewire component on the server and sends its HTML (15–52 KB gzipped, 0.4–2 MB uncompressed), which the browser then morphs into the page. The gap grows with row count and with slower devices, and it would grow on a real network, where Filament’s responses are 5–7× larger after compression.
  • Without SSR, Filament puts rows on screen sooner on first load. Filament renders the table as HTML on the server. Without SSR, Spindle’s rows appear once the JavaScript has run: about 60 ms later on desktop and 230–260 ms later at 4× slowdown. With Inertia SSR the gap is gone (see above). Largest contentful paint is earlier for Spindle when the CPU is slow even without SSR, probably because Filament’s page is heavier to style and lay out (632 KB of uncompressed CSS against 140 KB).
  • Search waits for typing to pause for 250 ms in Spindle and 500 ms in Filament. Most of the 330 ms against 616 ms from the keystroke comes from those waits. From the moment the request starts, Spindle is still 10–40% faster.
  • Selecting all rows was faster in Filament in this run (1–11 ms against 8–43 ms). Both do it without a request; Spindle then re-checked every row against the whole selection. Selection checks are now constant-time (see Rendering), but this comparison has not been re-run since.
  • Total page weight is similar: about 356 KB gzipped for Spindle against 352 KB for Filament. Spindle’s JavaScript and fonts include the whole demo app (sidebar, starter-kit components, six font files), not just the table.

Setup

  • Machine: Apple M4, 16 GB, macOS 27, Chromium through Playwright 1.63, 1440×900 viewport. The “4×” profile uses Chrome’s CPU throttling.
  • Versions: PHP 8.4 with OPcache, Laravel 13.33, SQLite. Spindle runs on Inertia 3.4 with Svelte 5.57. Filament 5.9 runs on Livewire 4.4.
  • Server: PHP’s built-in server with APP_DEBUG=false, OPcache on without timestamp checks, and production-built assets. Filament’s production caches (php artisan filament:optimize) were on.
  • Database: a private copy of the demo database. In a trial run against the shared development copy, a few writes (session saves and a toggle) stalled for about 500 ms; with the private copy, every p90 stays close to its median.
  • Caveats:
    • The built-in server doesn’t compress responses, so gzip sizes are computed from the response bodies.
    • The server and browser run on the same machine, so there is no network latency.
    • Both are single-user measurements, not load tests.
    • The two pages have different chrome around the table (the starter-kit sidebar layout against the Filament panel).
  • How each metric is measured:
    • Rows on screen is the first animation frame after the first row appears in the DOM.
    • Interaction times run from the click, input or change event to the first frame after the rows change.
    • “Until saved” runs until the save request completes.