cloudflare / cloudflare/vinext
Investigate App Router dynamic SSR performance against Platformatic benchmark
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
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.
Contributor guide
Research direction
Start with packages/vinext/src/server/app-page-render.ts and trace probeAppPageBeforeRender through app-page-probe.ts and app-page-execution.ts, then inspect app-ssr-entry.ts. Add timing breakdowns for route matching, probing, RSC, SSR, and streaming, and verify whether page data runs once or twice. Re-run the local benchmark using the .docker/ harness and docker-compose.local-bench.yml; done means the avoidable probe cost is confirmed or removed safely and results are compared with the requested EKS setup.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, next.js, react, typescript
- Domain
- backend, performance, web-dev
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100