Skip to content

Investigate App Router dynamic SSR performance against Platformatic benchmark #933

@NathanDrake2406

Description

@NathanDrake2406

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions