[GH-ISSUE #933] Investigate App Router dynamic SSR performance against Platformatic benchmark #203

Open
opened 2026-05-06 12:38:08 +02:00 by BreizhHardware · 0 comments

Originally created by @NathanDrake2406 on GitHub (Apr 28, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/933

I saw this benchmark/tweet and tried benchmarking vinext. There are very clear improvement opportunities for us here.

What This Tests

The benchmark mostly tests dynamic page SSR under load.

For vinext/Next.js, that means:

route match -> build RSC tree -> render Flight -> SSR consumes Flight -> HTML

It does not meaningfully test API routes, middleware, static assets, hydration, or client navigation.

Local Docker Harness

I added a local Docker Compose harness as a closer middle ground to the Platformatic setup:

  • 6 app containers behind nginx
  • k6 in a separate container
  • repeated runs with randomized framework order
  • warmup before each measured run
  • median summary across runs
  • reports completed app iterations/sec, not raw HTTP requests/sec
  • same benchmark route mix

This is still not apples-to-apples with Platformatic EKS. Docker Desktop shares CPU/networking with the load generator. Treat this as a local saturation signal, not publishable framework ranking.

Versions:

  • Next.js 16.2.4, React 19.2.5
  • React Router 7.14.2, React 19.2.5, Vite 8.0.10
  • TanStack Start 1.167.50, TanStack Router 1.168.25, React 19.2.5, Vite 8.0.10
  • vinext latest local build from /Users/nathan/Projects/vinext, React 19.2.5, Vite 8.0.10

Result: 1000 rps Baseline

One corrected baseline run at 1000 rps, 6 replicas, 120s measured duration:

Framework Success Rate Achieved rps Avg Latency Median p99 Dropped
TanStack Start 100.0% 999.9 9ms 9ms 20ms 0
React Router 100.0% 999.9 10ms 9ms 20ms 0
Next.js 100.0% 999.9 13ms 11ms 47ms 0
vinext 100.0% 993.7 351ms 45ms 11.58s 705

Interpretation: 1000 rps is too easy for React Router, TanStack, and Next in this local harness. It already stresses vinext: completed requests return 200, but tail latency spikes and k6 drops scheduled iterations.

Result: 2000 rps Saturation

Three corrected randomized-order runs at 2000 rps, 6 replicas, 120s measured duration:

Framework Runs Success Rate Achieved rps Avg Latency Median p99 Dropped
React Router 3 100.0% 1999.7 13ms 11ms 43ms 0
TanStack Start 3 100.0% 1999.6 16ms 12ms 78ms 0
Next.js 3 100.0% 1163.7 3.71s 648ms 33.76s 84,525
vinext 3 100.0% 975.5 4.52s 884ms 37.26s 109,679

Randomized order:

Round 1: react-router next tanstack vinext
Round 2: next tanstack vinext react-router
Round 3: react-router vinext next tanstack

Interpretation:

  • React Router and TanStack remain clean at 2000 rps.
  • Next.js saturates around ~1.16k rps in this local harness.
  • vinext saturates lower, around ~975 rps, with worse tail latency and more dropped iterations.
  • The useful signal is not the absolute numbers. The useful signal is the shape: vinext behaves like the expensive dynamic App Router SSR path and falls over well before loader-style frameworks.

Harness files live in .docker/ and docker-compose.local-bench.yml in the local benchmark checkout.

Likely vinext Bottleneck

The first suspicious vinext-specific cost is duplicate work in the dynamic App Router page path:

probe page/layouts
-> await async page work when there is no loading.tsx
-> real RSC render executes the page again
-> SSR consumes the Flight stream
-> embed Flight back into HTML

Files to inspect:

  • packages/vinext/src/server/app-page-render.ts - always calls probeAppPageBeforeRender() before render
  • packages/vinext/src/server/app-page-probe.ts - probes layouts and page before render
  • packages/vinext/src/server/app-page-execution.ts - awaits async page probe results when no loading boundary exists
  • packages/vinext/src/server/app-ssr-entry.ts - tees Flight and feeds SSR/html embedding

Done When

  • Add timing breakdowns for route match, probe, RSC render, SSR render, and streaming.
  • Confirm whether benchmark page data work executes once or twice per request.
  • Remove or bypass avoidable probe work for normal force-dynamic page SSR if compatible.
  • Re-run the benchmark port locally and then in an apples-to-apples EKS setup.
Originally created by @NathanDrake2406 on GitHub (Apr 28, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/933 I saw this benchmark/tweet and tried benchmarking vinext. There are very clear improvement opportunities for us here. - Blog: https://blog.platformatic.dev/ssr-framework-benchmarks-v2-corrected-results - Tweet: https://x.com/matteocollina/status/2036502852642713922 - Benchmark repo: https://github.com/platformatic/k8s-watt-performance-demo/tree/ecommerce ## What This Tests The benchmark mostly tests **dynamic page SSR under load**. For vinext/Next.js, that means: ```text route match -> build RSC tree -> render Flight -> SSR consumes Flight -> HTML ``` It does not meaningfully test API routes, middleware, static assets, hydration, or client navigation. ## Local Docker Harness I added a local Docker Compose harness as a closer middle ground to the Platformatic setup: - 6 app containers behind nginx - k6 in a separate container - repeated runs with randomized framework order - warmup before each measured run - median summary across runs - reports completed app iterations/sec, not raw HTTP requests/sec - same benchmark route mix This is still **not apples-to-apples with Platformatic EKS**. Docker Desktop shares CPU/networking with the load generator. Treat this as a local saturation signal, not publishable framework ranking. Versions: - Next.js `16.2.4`, React `19.2.5` - React Router `7.14.2`, React `19.2.5`, Vite `8.0.10` - TanStack Start `1.167.50`, TanStack Router `1.168.25`, React `19.2.5`, Vite `8.0.10` - vinext latest local build from `/Users/nathan/Projects/vinext`, React `19.2.5`, Vite `8.0.10` ## Result: 1000 rps Baseline One corrected baseline run at `1000 rps`, 6 replicas, 120s measured duration: | Framework | Success Rate | Achieved rps | Avg Latency | Median | p99 | Dropped | | --- | ---: | ---: | ---: | ---: | ---: | ---: | | TanStack Start | 100.0% | 999.9 | 9ms | 9ms | 20ms | 0 | | React Router | 100.0% | 999.9 | 10ms | 9ms | 20ms | 0 | | Next.js | 100.0% | 999.9 | 13ms | 11ms | 47ms | 0 | | **vinext** | **100.0%** | **993.7** | **351ms** | **45ms** | **11.58s** | **705** | Interpretation: `1000 rps` is too easy for React Router, TanStack, and Next in this local harness. It already stresses vinext: completed requests return 200, but tail latency spikes and k6 drops scheduled iterations. ## Result: 2000 rps Saturation Three corrected randomized-order runs at `2000 rps`, 6 replicas, 120s measured duration: | Framework | Runs | Success Rate | Achieved rps | Avg Latency | Median | p99 | Dropped | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | React Router | 3 | 100.0% | 1999.7 | 13ms | 11ms | 43ms | 0 | | TanStack Start | 3 | 100.0% | 1999.6 | 16ms | 12ms | 78ms | 0 | | Next.js | 3 | 100.0% | 1163.7 | 3.71s | 648ms | 33.76s | 84,525 | | **vinext** | **3** | **100.0%** | **975.5** | **4.52s** | **884ms** | **37.26s** | **109,679** | Randomized order: ```text Round 1: react-router next tanstack vinext Round 2: next tanstack vinext react-router Round 3: react-router vinext next tanstack ``` Interpretation: - React Router and TanStack remain clean at `2000 rps`. - Next.js saturates around ~`1.16k rps` in this local harness. - vinext saturates lower, around ~`975 rps`, with worse tail latency and more dropped iterations. - The useful signal is not the absolute numbers. The useful signal is the shape: vinext behaves like the expensive dynamic App Router SSR path and falls over well before loader-style frameworks. Harness files live in `.docker/` and `docker-compose.local-bench.yml` in the local benchmark checkout. ## Likely vinext Bottleneck The first suspicious vinext-specific cost is duplicate work in the dynamic App Router page path: ```text probe page/layouts -> await async page work when there is no loading.tsx -> real RSC render executes the page again -> SSR consumes the Flight stream -> embed Flight back into HTML ``` Files to inspect: - `packages/vinext/src/server/app-page-render.ts` - always calls `probeAppPageBeforeRender()` before render - `packages/vinext/src/server/app-page-probe.ts` - probes layouts and page before render - `packages/vinext/src/server/app-page-execution.ts` - awaits async page probe results when no loading boundary exists - `packages/vinext/src/server/app-ssr-entry.ts` - tees Flight and feeds SSR/html embedding ## Done When - Add timing breakdowns for route match, probe, RSC render, SSR render, and streaming. - Confirm whether benchmark page data work executes once or twice per request. - Remove or bypass avoidable probe work for normal `force-dynamic` page SSR if compatible. - Re-run the benchmark port locally and then in an apples-to-apples EKS setup.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#203
No description provided.