Run Time Stats
Client Side Rendered Tests
First Paint (ms)
First Contentful Paint (ms)
Interaction to Next Paint (ms)
| Framework | First Paint | FCP | INP |
|---|---|---|---|
| Astro | 89ms | 88.93ms | 1.92ms |
| Next.js | 347.2ms | 347.24ms | 20.16ms |
| Nuxt | 163ms | 163.12ms | 11.15ms |
| React Router | 155.4ms | 155.62ms | 15.52ms |
| SolidStart | 105ms | 104.65ms | 18.32ms |
| SvelteKit | 112.2ms | 112.18ms | 9.38ms |
| TanStack Start | 148.8ms | 148.68ms | 26.36ms |
Methodology
- Each framework renders a table of 1000 rows with two UUID columns
- Measured using Lighthouse flow with Chromium via Puppeteer for accurate browser metrics
- First Paint and First Contentful Paint are measured on initial navigation
- Interaction to Next Paint is measured by clicking the first row's detail link
- Benchmarks run 5 times and results are averaged
- Next.js wraps the client-side rendered table in a
dynamicimport withssr: falseto prevent build-time prerendering - TanStack Start, Nuxt, SvelteKit, and SolidStart disable SSR per-route
- React Router uses route-level
clientLoaderfunctions withHydrateFallbackso the client-rendered routes are not server-rendered - Astro uses client-only React islands for client-side rendered routes
- Client-side rendered tests use each framework's normal production build because SPA-only build modes are not supported consistently across the frameworks being compared
- Astro uses React for its client-side rendered test: the benchmark table and detail components are React islands rendered with
client:only="react", which prevents Astro from server-rendering those components and lets them render only in the browser. Astro'sClientRouteris not used for this CSR test because it enables client-side transitions and soft navigation behavior rather than client-only rendering.
Server Side Rendered Tests
First Paint (ms)
First Contentful Paint (ms)
Interaction to Next Paint (ms)
| Framework | First Paint | FCP | INP |
|---|---|---|---|
| Astro | 69ms | 68.85ms | 0.63ms |
| Next.js | 132.6ms | 132.6ms | 17.31ms |
| Nuxt | 90ms | 89.98ms | 12.08ms |
| React Router | 95.6ms | 95.55ms | 20.62ms |
| SolidStart | 100.6ms | 100.61ms | 15.33ms |
| SvelteKit | 88.2ms | 88.3ms | 14.08ms |
| TanStack Start | 120.6ms | 120.69ms | 29.5ms |
Methodology
- Each framework renders a table of 1000 rows with two UUID columns
- Measured using Lighthouse flow with Chromium via Puppeteer for accurate browser metrics
- First Paint and First Contentful Paint are measured on initial navigation
- Interaction to Next Paint is measured by clicking the first row's detail link
- Benchmarks run 5 times and results are averaged
- The measured route is
/server-side-rendered, and detail navigation uses/server-side-rendered/:id.
Server Side Throughput Tests
Ops/sec
Body Size
Median Latency
| Framework | Ops/sec | Median Latency | Body Size | Duplication |
|---|---|---|---|---|
| Baseline HTML | 841 | 1.206ms | 96.83kb | 1x |
| Astro | 529 | 1.836ms | 99.82kb | 1x |
| Mastro | 515 | 1.912ms | 181.95kb | 1x |
| Next.js | 217 | 4.819ms | 199.57kb | 2x |
| Nuxt | 362 | 2.675ms | 201.27kb | 2x |
| React Router | 345 | 2.868ms | 211.63kb | 2x |
| SolidStart | 405 | 2.539ms | 230.24kb | 2x |
| SvelteKit | 424 | 2.284ms | 183.49kb | 2x |
| TanStack Start | 315 | 3.146ms | 193.62kb | 2x |
Methodology
- Each framework renders the dedicated
/ssr-throughputroute with a table of 1000 rows and UUID id/name columns - This route intentionally does not render the exact same table as the browser SSR and load tests: it omits detail links and framework link components so router, prefetch, and navigation metadata do not dominate the request-handler throughput measurement
- Mock HTTP requests bypass TCP overhead so this measures request-handler rendering throughput rather than full server throughput
- Data is loaded asynchronously to simulate real-world data fetching
- Duplication factor indicates how many times each UUID appears in the response (1x = optimal, 2x = includes hydration payload)
- Benchmarks run for 10 seconds using tinybench
- Frameworks are invoked through their production request handlers where possible. Web API handlers are called with
Requestobjects; Node.js handlers are called with mockIncomingMessageandServerResponseobjects. - Next.js renders the throughput table as a client component, matching the setup from PR #94, so the benchmark compares traditional server-rendered React + hydration work instead of making Next.js render every table row as React Server Components
- Inspired by eknkc/ssr-benchmark
Server Side Load Test
P99 Latency
P99 Latency at 25 Connections
P99 Latency at 50 Connections
P99 Latency at 100 Connections
P90 Latency
P90 Latency at 25 Connections
P90 Latency at 50 Connections
P90 Latency at 100 Connections
| Framework | Peak req/s | Peak Connections | P99 @ 25 | P99 @ 50 | P99 @ 100 | Total Req. |
|---|---|---|---|---|---|---|
| Baseline HTML | 1,629.8 | 50 | 20ms | 44ms | 107ms | 48,210 |
| Astro | 693.6 | 25 | 58ms | 116ms | 1710ms | 21,198 |
| Next.js | 34.4 | 1 | 2154ms | 4693ms | 4754ms | 1,000 |
| Nuxt | 66 | 25 | 2051ms | 4092ms | 4375ms | 2,088 |
| React Router | 176 | 50 | 269ms | 1633ms | 3971ms | 5,724 |
| SolidStart | 60 | 10 | 3770ms | 4055ms | 4175ms | 1,944 |
| SvelteKit | 497.4 | 25 | 78ms | 290ms | 2542ms | 15,662 |
| TanStack Start | 42 | 5 | 4429ms | 4150ms | 4434ms | 1,289 |
Methodology
- Each framework serves the server-rendered table route over a real local HTTP server
- The measured route is
/server-side-rendered, using the same 1000-row UUID table as the SSR request throughput and browser rendering tests - Load is applied in staged connection counts, from 1 through 200 concurrent connections, with each stage running for approximately 5 seconds
- Peak requests/sec is the highest successful stage throughput observed during the staged run
- P90 and P99 latency are compared at the 25-, 50-, and 100-connection stages for every framework, so latency is measured under the same concurrency pressure
- Total requests cover the full staged load run, not only the peak stage
Core Web Vitals Desktop
Good Largest Contentful Paint
Measures how fast a page's main content loads. To provide a good user experience, the LCP should be 2.5 seconds or less.
Good Cumulative Layout Shift
Measures how much and how far content unexpectedly moves on a page. To provide a good user experience, sites should maintain a CLS score of 0.1 or less for at least 75% of page visits.
Good First Contentful Paint
Measures initial loading speed. To provide a good user experience, the FCP should be 1.8 seconds or less.
Good Time To First Byte
Measures the time between the request for a resource and when the first byte of a response begins to arrive. A good TTFB is less than or equal to 800ms.
Good Interaction to Next Paint
Measures overall page responsiveness to user actions. To provide a good user experience, the INP should be 200ms or less.
| Framework | LCP% | CLS% | FCP% | TTFB% | INP% |
|---|---|---|---|---|---|
| SolidStart | 91 | 66 | 91 | 83 | 100 |
| Astro | 91 | 87 | 91 | 82 | 97 |
| Nuxt.js | 65 | 57 | 67 | 59 | 95 |
| SvelteKit | 79 | 77 | 78 | 71 | 94 |
| Next.js | 72 | 69 | 76 | 67 | 94 |
| React Router | 61 | 62 | 66 | 70 | 90 |
Methodology
- All Core Web Vitals for Desktop are sourced from HTTP Archive
- Metrics refresh monthly