- Rendering: the Svelte table on its own, before and after the rendering work (virtualization and cheaper rows).
- Spindle Table vs Filament: twin tables in the demo app.
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.