From eab757035f4632ddd79d78e85e0b525b76fa6af2 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 00:02:21 +0200
Subject: [PATCH 01/24] benchmark: memory (codspeed)
---
.github/workflows/memory-benchmarks.yml | 49 +++
benchmarks/memory/README.md | 122 +++++++
benchmarks/memory/client/bench-utils.ts | 19 ++
benchmarks/memory/client/package.json | 68 ++++
.../react/memory.bench.ts | 51 +++
.../react/project.json | 31 ++
.../interrupted-navigations/react/setup.ts | 262 +++++++++++++++
.../interrupted-navigations/react/src/app.tsx | 37 +++
.../react/src/routeTree.gen.ts | 95 ++++++
.../react/src/router.tsx | 17 +
.../react/src/routes/__root.tsx | 9 +
.../react/src/routes/fast.$id.tsx | 21 ++
.../react/src/routes/index.tsx | 9 +
.../react/src/routes/slow.$id.tsx | 21 ++
.../react/src/slow-loaders.ts | 54 ++++
.../react/tsconfig.json | 18 ++
.../react/vite.config.ts | 34 ++
.../react/memory.bench.ts | 48 +++
.../loader-data-retention/react/project.json | 31 ++
.../loader-data-retention/react/setup.ts | 148 +++++++++
.../loader-data-retention/react/src/app.tsx | 33 ++
.../react/src/loader-data.ts | 48 +++
.../react/src/routeTree.gen.ts | 102 ++++++
.../react/src/router.tsx | 19 ++
.../react/src/routes/__root.tsx | 9 +
.../react/src/routes/page.$id.tsx | 21 ++
.../react/src/routes/shell.index.tsx | 9 +
.../react/src/routes/shell.tsx | 9 +
.../loader-data-retention/react/tsconfig.json | 18 ++
.../react/vite.config.ts | 34 ++
.../mount-unmount/react/memory.bench.ts | 21 ++
.../mount-unmount/react/project.json | 31 ++
.../scenarios/mount-unmount/react/setup.ts | 66 ++++
.../scenarios/mount-unmount/react/src/app.tsx | 29 ++
.../mount-unmount/react/src/routeTree.gen.ts | 59 ++++
.../mount-unmount/react/src/router.tsx | 17 +
.../mount-unmount/react/src/routes/__root.tsx | 9 +
.../mount-unmount/react/src/routes/a.tsx | 12 +
.../mount-unmount/react/tsconfig.json | 18 ++
.../mount-unmount/react/vite.config.ts | 34 ++
.../navigation-churn/react/memory.bench.ts | 34 ++
.../navigation-churn/react/project.json | 31 ++
.../scenarios/navigation-churn/react/setup.ts | 117 +++++++
.../navigation-churn/react/src/app.tsx | 31 ++
.../react/src/routeTree.gen.ts | 77 +++++
.../navigation-churn/react/src/router.tsx | 17 +
.../react/src/routes/__root.tsx | 9 +
.../navigation-churn/react/src/routes/a.tsx | 14 +
.../navigation-churn/react/src/routes/b.tsx | 14 +
.../navigation-churn/react/tsconfig.json | 18 ++
.../navigation-churn/react/vite.config.ts | 34 ++
.../preload-churn/react/memory.bench.ts | 55 ++++
.../preload-churn/react/project.json | 31 ++
.../scenarios/preload-churn/react/setup.ts | 239 ++++++++++++++
.../scenarios/preload-churn/react/src/app.tsx | 33 ++
.../preload-churn/react/src/item-payload.ts | 51 +++
.../preload-churn/react/src/routeTree.gen.ts | 77 +++++
.../preload-churn/react/src/router.tsx | 18 ++
.../preload-churn/react/src/routes/__root.tsx | 9 +
.../preload-churn/react/src/routes/index.tsx | 9 +
.../react/src/routes/items.$id.tsx | 20 ++
.../preload-churn/react/tsconfig.json | 18 ++
.../preload-churn/react/vite.config.ts | 34 ++
.../react/memory.bench.ts | 46 +++
.../unique-location-churn/react/project.json | 31 ++
.../unique-location-churn/react/setup.ts | 123 +++++++
.../unique-location-churn/react/src/app.tsx | 31 ++
.../react/src/routeTree.gen.ts | 59 ++++
.../react/src/router.tsx | 17 +
.../react/src/routes/__root.tsx | 9 +
.../react/src/routes/items.$id.tsx | 28 ++
.../unique-location-churn/react/tsconfig.json | 18 ++
.../react/vite.config.ts | 34 ++
benchmarks/memory/client/tsconfig.json | 7 +
.../memory/client/vitest.react.config.ts | 8 +
benchmarks/memory/client/vitest.setup.ts | 6 +
benchmarks/memory/server/bench-utils.ts | 63 ++++
benchmarks/memory/server/package.json | 68 ++++
.../aborted-requests/react/memory.bench.ts | 160 +++++++++
.../aborted-requests/react/project.json | 31 ++
.../react/src/routeTree.gen.ts | 86 +++++
.../aborted-requests/react/src/router.tsx | 16 +
.../react/src/routes/__root.tsx | 27 ++
.../react/src/routes/index.tsx | 9 +
.../react/src/routes/stream.$id.tsx | 77 +++++
.../aborted-requests/react/tsconfig.json | 14 +
.../aborted-requests/react/vite.config.ts | 29 ++
.../error-paths/react/memory.bench.ts | 201 ++++++++++++
.../scenarios/error-paths/react/project.json | 31 ++
.../error-paths/react/src/routeTree.gen.ts | 146 +++++++++
.../error-paths/react/src/router.tsx | 23 ++
.../error-paths/react/src/routes/__root.tsx | 27 ++
.../error-paths/react/src/routes/boom.$id.tsx | 17 +
.../error-paths/react/src/routes/from.$id.tsx | 14 +
.../error-paths/react/src/routes/index.tsx | 9 +
.../react/src/routes/missing.$id.tsx | 19 ++
.../react/src/routes/target.$id.tsx | 11 +
.../scenarios/error-paths/react/tsconfig.json | 14 +
.../error-paths/react/vite.config.ts | 29 ++
.../peak-large-page/react/memory.bench.ts | 76 +++++
.../peak-large-page/react/project.json | 31 ++
.../react/src/large-page-data.ts | 111 +++++++
.../react/src/routeTree.gen.ts | 305 ++++++++++++++++++
.../peak-large-page/react/src/router.tsx | 16 +
.../react/src/routes/__root.tsx | 27 ++
.../react/src/routes/index.tsx | 9 +
.../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 25 ++
.../react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 26 ++
.../react/src/routes/l1.l2.l3.l4.l5.l6.tsx | 26 ++
.../react/src/routes/l1.l2.l3.l4.l5.tsx | 26 ++
.../react/src/routes/l1.l2.l3.l4.tsx | 26 ++
.../react/src/routes/l1.l2.l3.tsx | 26 ++
.../react/src/routes/l1.l2.tsx | 26 ++
.../peak-large-page/react/src/routes/l1.tsx | 26 ++
.../peak-large-page/react/tsconfig.json | 14 +
.../peak-large-page/react/vite.config.ts | 29 ++
.../request-churn/react/memory.bench.ts | 83 +++++
.../request-churn/react/project.json | 31 ++
.../request-churn/react/src/routeTree.gen.ts | 86 +++++
.../request-churn/react/src/router.tsx | 16 +
.../request-churn/react/src/routes/__root.tsx | 27 ++
.../request-churn/react/src/routes/index.tsx | 9 +
.../react/src/routes/items.$id.tsx | 40 +++
.../request-churn/react/tsconfig.json | 14 +
.../request-churn/react/vite.config.ts | 29 ++
.../react/memory.bench.ts | 105 ++++++
.../serialization-payload/react/project.json | 31 ++
.../react/src/routeTree.gen.ts | 68 ++++
.../react/src/router.tsx | 16 +
.../react/src/routes/__root.tsx | 27 ++
.../react/src/routes/data.$id.tsx | 130 ++++++++
.../serialization-payload/react/tsconfig.json | 14 +
.../react/vite.config.ts | 29 ++
.../server-fn-churn/react/memory.bench.ts | 261 +++++++++++++++
.../server-fn-churn/react/project.json | 31 ++
.../server-fn-churn/react/src/fns.ts | 47 +++
.../react/src/routeTree.gen.ts | 86 +++++
.../server-fn-churn/react/src/router.tsx | 16 +
.../react/src/routes/__root.tsx | 27 ++
.../react/src/routes/api.fn-urls.ts | 14 +
.../react/src/routes/index.tsx | 18 ++
.../server-fn-churn/react/tsconfig.json | 14 +
.../server-fn-churn/react/vite.config.ts | 29 ++
.../streaming-peak/react/memory.bench.ts | 217 +++++++++++++
.../streaming-peak/react/project.json | 31 ++
.../streaming-peak/react/src/routeTree.gen.ts | 86 +++++
.../streaming-peak/react/src/router.tsx | 16 +
.../react/src/routes/__root.tsx | 27 ++
.../streaming-peak/react/src/routes/index.tsx | 9 +
.../react/src/routes/stream.$id.tsx | 107 ++++++
.../streaming-peak/react/tsconfig.json | 14 +
.../streaming-peak/react/vite.config.ts | 29 ++
benchmarks/memory/server/tsconfig.json | 7 +
.../memory/server/vitest.react.config.ts | 8 +
.../src/ssr/renderRouterToStream.tsx | 14 +-
pnpm-lock.yaml | 87 ++++-
pnpm-workspace.yaml | 1 +
157 files changed, 7042 insertions(+), 4 deletions(-)
create mode 100644 .github/workflows/memory-benchmarks.yml
create mode 100644 benchmarks/memory/README.md
create mode 100644 benchmarks/memory/client/bench-utils.ts
create mode 100644 benchmarks/memory/client/package.json
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/project.json
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/project.json
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/project.json
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/project.json
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/project.json
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts
create mode 100644 benchmarks/memory/client/tsconfig.json
create mode 100644 benchmarks/memory/client/vitest.react.config.ts
create mode 100644 benchmarks/memory/client/vitest.setup.ts
create mode 100644 benchmarks/memory/server/bench-utils.ts
create mode 100644 benchmarks/memory/server/package.json
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/project.json
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/project.json
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/project.json
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/project.json
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/project.json
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/project.json
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/project.json
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts
create mode 100644 benchmarks/memory/server/tsconfig.json
create mode 100644 benchmarks/memory/server/vitest.react.config.ts
diff --git a/.github/workflows/memory-benchmarks.yml b/.github/workflows/memory-benchmarks.yml
new file mode 100644
index 0000000000..a7c23f8a1b
--- /dev/null
+++ b/.github/workflows/memory-benchmarks.yml
@@ -0,0 +1,49 @@
+# Setup taken from https://codspeed.io/docs/benchmarks/nodejs/vitest
+name: Memory Benchmarks
+
+on:
+ push:
+ branches:
+ - 'main'
+ paths:
+ - 'packages/**'
+ - 'benchmarks/**'
+ pull_request:
+ paths:
+ - 'packages/**'
+ - 'benchmarks/**'
+ workflow_dispatch:
+
+permissions:
+ contents: read # required for actions/checkout
+ id-token: write # required for OIDC authentication with CodSpeed
+
+env:
+ NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
+ SERVER_PRESET: 'node-server'
+ NX_NO_CLOUD: true
+
+jobs:
+ benchmarks:
+ name: Run ${{ matrix.benchmark }} CodSpeed benchmark
+ strategy:
+ fail-fast: false
+ matrix:
+ benchmark:
+ - memory-server
+ - memory-client
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Tools
+ uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main
+
+ - name: Run ${{ matrix.benchmark }} CodSpeed benchmark
+ uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
+ with:
+ mode: memory
+ run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:react
diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md
new file mode 100644
index 0000000000..c03f112329
--- /dev/null
+++ b/benchmarks/memory/README.md
@@ -0,0 +1,122 @@
+# Memory Benchmarks
+
+Dedicated memory benchmarks for TanStack Router / Start, measured with the
+CodSpeed **memory instrument** (`mode: memory` in
+`.github/workflows/memory-benchmarks.yml`). Two separate benchmarks:
+
+- `server/` (`@benchmarks/memory-server`) — React Start apps, requests against
+ the built server handler (`handler.fetch`), Node environment.
+- `client/` (`@benchmarks/memory-client`) — router-only React apps in jsdom.
+
+These deliberately do **not** reuse the CPU scenarios in `benchmarks/ssr` and
+`benchmarks/client-nav`: memory benches need their own iteration counts,
+payload sizes, and route shapes, and tuning those must never shift the CPU
+baselines. React-first; each scenario keeps a `react/` level so solid/vue can
+be added later without renames.
+
+## Layout
+
+```text
+benchmarks/memory//
+ package.json Nx targets: build:react, test:perf:react, test:types
+ bench-utils.ts memoryBenchOptions, seeded LCG (+ sequential request loop on the server side)
+ vitest.react.config.ts aggregates scenarios/*/react/vite.config.ts
+ scenarios//react/ one isolated app per scenario + memory.bench.ts
+```
+
+One app per scenario; apps and bench names are stable once landed (CodSpeed
+continuity). Never grow an existing scenario for a new case — add a scenario.
+
+## How the memory instrument executes a bench
+
+- The bench function is warmed up, then **measured exactly once**, starting
+ after a forced GC. Under plain `vitest bench` the suites only smoke-test:
+ timing output is meaningless; real numbers come from CodSpeed.
+- Under CodSpeed the bench fn runs several warmup invocations plus the
+ measured one **on the same mount**, so bench fns must be idempotent and
+ module-level counters/LCGs are used where ids must never repeat across
+ invocations.
+- Plain `vitest bench` never runs suite hooks (`beforeAll`/`afterAll`) and
+ only honors tinybench's `setup`/`teardown` options; the CodSpeed runner
+ does the exact opposite. Client benches therefore register **both** — in
+ any given mode exactly one pair runs.
+- The process runs with V8 determinism flags (predictable GC schedule,
+ `--no-opt`). Never call `global.gc()` manually. Because of `--no-opt`,
+ allocation counts overstate production; numbers are for regression
+ tracking, not absolute claims.
+- Keep each bench under **~1.5M allocations** (instrument overhead grows past
+ 2M); this is the main constraint when tuning iteration counts.
+
+## Bench shapes and signals
+
+- **Churn (leak detector):** N sequential iterations at steady state. If one
+ iteration leaks L bytes, peak grows by ~N·L; healthy builds show a flat
+ timeline floor independent of N. Tuning check: doubling N must leave peak
+ roughly unchanged.
+- **Peak (footprint):** one (or very few) large operations; peak memory
+ scaling with the workload is the signal.
+
+## Scenarios
+
+### Server
+
+| Scenario | Shape | Guards against |
+| ----------------------- | ----- | --------------------------------------------------------- |
+| `request-churn` | churn | cross-request retention in document SSR (unique URLs) |
+| `server-fn-churn` | churn | retention in the server-function RPC path |
+| `error-paths` | churn | redirect/notFound/error/unmatched paths pinning contexts |
+| `aborted-requests` | churn | dangling streams/listeners after mid-stream client aborts |
+| `peak-large-page` | peak | per-request peak scaling with page size |
+| `streaming-peak` | peak | streaming buffering O(document) instead of O(chunk) |
+| `serialization-payload` | peak | double-buffering / string-copy blowups in dehydration |
+
+### Client
+
+| Scenario | Shape | Guards against |
+| ------------------------- | ----- | -------------------------------------------------------- |
+| `navigation-churn` | churn | per-navigation retention at steady state |
+| `unique-location-churn` | churn | unbounded href/search-keyed caches (never-repeated URLs) |
+| `preload-churn` | churn | preload-cache eviction not releasing memory |
+| `loader-data-retention` | churn | departed routes' loader data staying pinned (gcTime 0) |
+| `mount-unmount` | churn | router instances not collectable after dispose |
+| `interrupted-navigations` | churn | superseded navigations retaining closures/contexts |
+
+## Conventions
+
+- Strictly sequential work: at most one request/navigation in flight; each
+ server response is fully consumed before the next request. Pairing a single
+ navigation with its render signal via `Promise.all([navigate, rendered])`
+ is fine — never overlap distinct work items.
+- Randomness only via the seeded LCG in `bench-utils.ts`; no `Math.random`,
+ `Date.now`, or timers — with one documented exception: `streaming-peak`'s
+ deferred sections use small `setTimeout` delays, because React schedules
+ stream flushes via `setImmediate` and any non-timer deferral wins that race,
+ suppressing the Suspense fallbacks the scenario exists to stream.
+- Sanity assertions run once at module load and throw on wrong
+ status/markers, so a bench can never silently measure the wrong thing.
+- Server requests follow `benchmarks/ssr` conventions: document GETs send
+ `accept: text/html`, server-fn requests send `sec-fetch-site: same-origin`
+ with bodies precomputed at module level.
+- Client apps export `mountTestApp` from `app.tsx`; benches import the built
+ `dist/app.js`; navigations use `replace: true`; unmount does full teardown
+ (React root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads
+ are never rendered into the DOM.
+- `NODE_ENV=production` everywhere (the Nx targets set it).
+
+## Run
+
+```bash
+pnpm nx run @benchmarks/memory-server:test:perf:react --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-client:test:perf:react --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-server:test:types --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemoteCache
+```
+
+Real memory measurement, locally (requires the CodSpeed CLI, `codspeed setup`
+once to install the memory executor, and sudo; **uploads results to the
+CodSpeed dashboard** — local runs do not affect PR baselines):
+
+```bash
+WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:react
+WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:react
+```
diff --git a/benchmarks/memory/client/bench-utils.ts b/benchmarks/memory/client/bench-utils.ts
new file mode 100644
index 0000000000..28bfe6c512
--- /dev/null
+++ b/benchmarks/memory/client/bench-utils.ts
@@ -0,0 +1,19 @@
+export const memoryBenchOptions = {
+ iterations: 1,
+ warmupIterations: 1,
+ time: 0,
+ warmupTime: 0,
+}
+
+export function createDeterministicRandom(seed: number) {
+ let state = seed >>> 0
+
+ return () => {
+ state = (state * 1664525 + 1013904223) >>> 0
+ return state / 0x100000000
+ }
+}
+
+export function randomSegment(random: () => number) {
+ return Math.floor(random() * 1_000_000_000).toString(36)
+}
diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json
new file mode 100644
index 0000000000..1bb60b0c1c
--- /dev/null
+++ b/benchmarks/memory/client/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "@benchmarks/memory-client",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/router-core": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@codspeed/vitest-plugin": "^5.5.0",
+ "@testing-library/react": "^16.2.0",
+ "@vitejs/plugin-react": "^6.0.1",
+ "@types/jsdom": "28.0.0",
+ "typescript": "^6.0.2",
+ "vite": "^8.0.14",
+ "vitest": "^4.1.4"
+ },
+ "nx": {
+ "targets": {
+ "build:react": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-react",
+ "@benchmarks/memory-client-unique-location-churn-react",
+ "@benchmarks/memory-client-preload-churn-react",
+ "@benchmarks/memory-client-loader-data-retention-react",
+ "@benchmarks/memory-client-mount-unmount-react",
+ "@benchmarks/memory-client-interrupted-navigations-react"
+ ],
+ "target": "build:client"
+ }
+ ]
+ },
+ "test:perf:react": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ "build:react"
+ ],
+ "options": {
+ "command": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts",
+ "cwd": "benchmarks/memory/client"
+ }
+ },
+ "test:types": {
+ "executor": "nx:noop",
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-react",
+ "@benchmarks/memory-client-unique-location-churn-react",
+ "@benchmarks/memory-client-preload-churn-react",
+ "@benchmarks/memory-client-loader-data-retention-react",
+ "@benchmarks/memory-client-mount-unmount-react",
+ "@benchmarks/memory-client-interrupted-navigations-react"
+ ],
+ "target": "test:types:client"
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts
new file mode 100644
index 0000000000..bc4c8ca6cd
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts
@@ -0,0 +1,51 @@
+import { afterAll, beforeAll, bench, describe } from 'vitest'
+import {
+ createDeterministicRandom,
+ memoryBenchOptions,
+ randomSegment,
+} from '../../../bench-utils'
+import { setup } from './setup'
+
+const interruptedNavigationIterations = 150
+
+const interruptedNavigationPairs = createInterruptedNavigationPairs(
+ interruptedNavigationIterations,
+)
+
+function createInterruptedNavigationPairs(iterations: number) {
+ const random = createDeterministicRandom(13)
+
+ return Array.from({ length: iterations }, (_, index) => ({
+ slowId: `slow-${index}-${randomSegment(random)}`,
+ fastId: `fast-${index}-${randomSegment(random)}`,
+ }))
+}
+
+await setup().sanity()
+
+describe('memory', () => {
+ const test = setup()
+
+ /**
+ * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
+ * honors tinybench's setup/teardown options; the CodSpeed runner does the
+ * exact opposite. Both registrations are load-bearing — exactly one pair
+ * runs in any given mode.
+ */
+ beforeAll(test.before)
+ afterAll(test.after)
+
+ bench(
+ 'mem interrupted-navigations (react)',
+ async () => {
+ for (const pair of interruptedNavigationPairs) {
+ await test.interrupt(pair.slowId, pair.fastId)
+ }
+ },
+ {
+ ...memoryBenchOptions,
+ setup: test.before,
+ teardown: test.after,
+ },
+ )
+})
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json
new file mode 100644
index 0000000000..77ccf8d85a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-client-interrupted-navigations-react",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
new file mode 100644
index 0000000000..4cc8182b05
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
@@ -0,0 +1,262 @@
+import type * as App from './src/app'
+
+type NavigationSettlement =
+ | {
+ status: 'fulfilled'
+ value: void
+ }
+ | {
+ status: 'rejected'
+ reason: unknown
+ }
+
+const appModulePath = './dist/app.js'
+const {
+ mountTestApp,
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+} = (await import(/* @vite-ignore */ appModulePath)) as typeof App
+
+const uninitialized = () =>
+ Promise.reject(
+ new Error('interrupted-navigations benchmark is not initialized'),
+ )
+
+const uninitializedSettlement = () =>
+ Promise.resolve({
+ status: 'rejected',
+ reason: new Error('interrupted-navigations benchmark is not initialized'),
+ })
+
+async function drainMicrotasks() {
+ await Promise.resolve()
+ await Promise.resolve()
+}
+
+function formatReason(reason: unknown) {
+ if (reason instanceof Error) {
+ return `${reason.name}: ${reason.message}`
+ }
+
+ return String(reason)
+}
+
+function assertSlowNavigationSettlement(settlement: NavigationSettlement) {
+ if (settlement.status === 'fulfilled') {
+ if (settlement.value !== undefined) {
+ throw new Error('Expected slow navigation to fulfill with undefined')
+ }
+
+ return
+ }
+
+ if (
+ reasonHasAbortShape(settlement.reason) ||
+ reasonHasCancellationShape(settlement.reason)
+ ) {
+ return
+ }
+
+ throw new Error(
+ `Expected slow navigation to settle as void or cancellation, got ${formatReason(
+ settlement.reason,
+ )}`,
+ )
+}
+
+function reasonHasAbortShape(reason: unknown) {
+ return reason instanceof DOMException && reason.name === 'AbortError'
+}
+
+function reasonHasCancellationShape(reason: unknown) {
+ return (
+ reason instanceof Error &&
+ (reason.name === 'AbortError' || reason.name === 'CancelledError')
+ )
+}
+
+export function setup() {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
+ )
+ }
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let expectedRenderedPath: string | undefined = undefined
+ let navigateFast: (id: string) => Promise = uninitialized
+ let startSlowNavigation: (id: string) => Promise =
+ uninitializedSettlement
+ let getLatestLoadPromise: () => Promise | undefined = () => undefined
+
+ function assertRenderedPage(page: 'shell' | 'fast', id?: string) {
+ const element = container?.querySelector('[data-bench-page]')
+ const actualPage = element?.dataset.benchPage
+ const actualId = element?.dataset.benchId
+
+ if (actualPage !== page) {
+ throw new Error(`Expected rendered page ${page}, got ${actualPage}`)
+ }
+
+ if (id !== undefined && actualId !== id) {
+ throw new Error(`Expected rendered id ${id}, got ${actualId}`)
+ }
+ }
+
+ async function waitForRenderedPage(page: 'shell' | 'fast', id?: string) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedPage(page, id)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedPage(page, id)
+ }
+
+ async function waitForSlowLoader(id: string) {
+ for (let attempt = 0; attempt < 20; attempt++) {
+ if (slowLoaderRegistry.has(id)) {
+ return
+ }
+
+ await drainMicrotasks()
+ }
+
+ throw new Error(`Slow loader was not registered for id: ${id}`)
+ }
+
+ function waitForNextRender(pathname: string) {
+ expectedRenderedPath = pathname
+
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const { router } = mounted
+ unmount = mounted.unmount
+ getLatestLoadPromise = () => router.latestLoadPromise
+
+ unsub = router.subscribe('onRendered', (event) => {
+ if (
+ expectedRenderedPath &&
+ event.toLocation.pathname !== expectedRenderedPath
+ ) {
+ return
+ }
+
+ const resolve = resolveRendered
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ resolve()
+ })
+
+ navigateFast = async (id) => {
+ const rendered = waitForNextRender(`/fast/${id}`)
+ await Promise.all([
+ router.navigate({
+ to: '/fast/$id',
+ params: { id },
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ startSlowNavigation = (id) => {
+ const navigation = router.navigate({
+ to: '/slow/$id',
+ params: { id },
+ replace: true,
+ })
+
+ return navigation
+ .then((value): NavigationSettlement => ({ status: 'fulfilled', value }))
+ .catch(
+ (reason): NavigationSettlement => ({ status: 'rejected', reason }),
+ )
+ }
+
+ await router.load()
+ await waitForRenderedPage('shell')
+ }
+
+ function after() {
+ resolveAllSlowLoaders()
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ navigateFast = uninitialized
+ startSlowNavigation = uninitializedSettlement
+ getLatestLoadPromise = () => undefined
+ }
+
+ async function interrupt(
+ slowId: string,
+ fastId: string,
+ assertShape = false,
+ ) {
+ const slowNavigation = startSlowNavigation(slowId)
+
+ await waitForSlowLoader(slowId)
+ const slowLoadPromise = getLatestLoadPromise()
+
+ if (!slowLoadPromise) {
+ throw new Error(`Slow navigation did not start a load for id: ${slowId}`)
+ }
+
+ await navigateFast(fastId)
+ resolveSlowLoader(slowId)
+
+ const settlement = await slowNavigation
+ // Superseded loads currently always resolve; the guard keeps the bench
+ // alive if router-core ever starts rejecting them.
+ await slowLoadPromise.catch(() => undefined)
+ await drainMicrotasks()
+
+ if (assertShape) {
+ assertSlowNavigationSettlement(settlement)
+ assertRenderedPage('fast', fastId)
+ }
+ }
+
+ return {
+ before,
+ interrupt,
+ async sanity() {
+ await before()
+
+ try {
+ assertRenderedPage('shell')
+ await interrupt('sanity-slow', 'sanity-fast', true)
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx
new file mode 100644
index 0000000000..c77230007a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx
@@ -0,0 +1,37 @@
+import { RouterProvider } from '@tanstack/react-router'
+import { createRoot } from 'react-dom/client'
+import { getRouter } from './router'
+
+export {
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+} from './slow-loaders'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const reactRoot = createRoot(container)
+ let didUnmount = false
+
+ reactRoot.render()
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ reactRoot.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..475085a433
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts
@@ -0,0 +1,95 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as SlowIdRouteImport } from './routes/slow.$id'
+import { Route as FastIdRouteImport } from './routes/fast.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const SlowIdRoute = SlowIdRouteImport.update({
+ id: '/slow/$id',
+ path: '/slow/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FastIdRoute = FastIdRouteImport.update({
+ id: '/fast/$id',
+ path: '/fast/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/fast/$id' | '/slow/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/fast/$id' | '/slow/$id'
+ id: '__root__' | '/' | '/fast/$id' | '/slow/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ FastIdRoute: typeof FastIdRoute
+ SlowIdRoute: typeof SlowIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/slow/$id': {
+ id: '/slow/$id'
+ path: '/slow/$id'
+ fullPath: '/slow/$id'
+ preLoaderRoute: typeof SlowIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/fast/$id': {
+ id: '/fast/$id'
+ path: '/fast/$id'
+ fullPath: '/fast/$id'
+ preLoaderRoute: typeof FastIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ FastIdRoute: FastIdRoute,
+ SlowIdRoute: SlowIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx
new file mode 100644
index 0000000000..ac27b20f79
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..889395056b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx
new file mode 100644
index 0000000000..146b266f1b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { fixedTimestamp } from '../slow-loaders'
+
+export const Route = createFileRoute('/fast/$id')({
+ loader: ({ params }) => ({
+ id: params.id,
+ kind: 'fast' as const,
+ ts: fixedTimestamp,
+ }),
+ component: FastComponent,
+})
+
+function FastComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.kind}:${data.id}:${data.ts}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx
new file mode 100644
index 0000000000..a425ac79c4
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return shell
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx
new file mode 100644
index 0000000000..15c7d996dc
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { getSlowLoaderDeferred } from '../slow-loaders'
+
+export const Route = createFileRoute('/slow/$id')({
+ loader: async ({ params }) => {
+ const deferred = getSlowLoaderDeferred(params.id)
+
+ return await deferred.promise
+ },
+ component: SlowComponent,
+})
+
+function SlowComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.kind}:${data.id}:${data.ts}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts
new file mode 100644
index 0000000000..c70ccac3da
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts
@@ -0,0 +1,54 @@
+export const fixedTimestamp = 1_700_000_000_000
+
+type SlowLoaderPayload = {
+ id: string
+ kind: 'slow'
+ ts: number
+}
+
+type SlowLoaderDeferred = {
+ promise: Promise
+ resolve: () => void
+}
+
+export const slowLoaderRegistry = new Map()
+
+export function getSlowLoaderDeferred(id: string) {
+ const existing = slowLoaderRegistry.get(id)
+
+ if (existing) {
+ return existing
+ }
+
+ let resolvePromise!: (payload: SlowLoaderPayload) => void
+ const promise = new Promise((resolve) => {
+ resolvePromise = resolve
+ })
+
+ const deferred: SlowLoaderDeferred = {
+ promise,
+ resolve() {
+ slowLoaderRegistry.delete(id)
+ resolvePromise({ id, kind: 'slow', ts: fixedTimestamp })
+ },
+ }
+
+ slowLoaderRegistry.set(id, deferred)
+ return deferred
+}
+
+export function resolveSlowLoader(id: string) {
+ const deferred = slowLoaderRegistry.get(id)
+
+ if (!deferred) {
+ throw new Error(`No pending slow loader for id: ${id}`)
+ }
+
+ deferred.resolve()
+}
+
+export function resolveAllSlowLoaders() {
+ for (const id of Array.from(slowLoaderRegistry.keys())) {
+ resolveSlowLoader(id)
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json
new file mode 100644
index 0000000000..26767bd491
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts
new file mode 100644
index 0000000000..048a9cc833
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client interrupted-navigations (react)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts
new file mode 100644
index 0000000000..331c74fb2a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts
@@ -0,0 +1,48 @@
+import { afterAll, beforeAll, bench, describe } from 'vitest'
+import {
+ createDeterministicRandom,
+ memoryBenchOptions,
+ randomSegment,
+} from '../../../bench-utils'
+import { setup } from './setup'
+
+const loaderDataRetentionNavigationCount = 20
+const pageIds = createPageIds()
+
+await setup().sanity()
+
+describe('memory', () => {
+ const test = setup()
+
+ /**
+ * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
+ * honors tinybench's setup/teardown options; the CodSpeed runner does the
+ * exact opposite. Both registrations are load-bearing — exactly one pair
+ * runs in any given mode.
+ */
+ beforeAll(test.before)
+ afterAll(test.after)
+
+ bench(
+ 'mem loader-data-retention (react)',
+ async () => {
+ for (const id of pageIds) {
+ await test.navigate(id)
+ }
+ },
+ {
+ ...memoryBenchOptions,
+ setup: test.before,
+ teardown: test.after,
+ },
+ )
+})
+
+function createPageIds() {
+ const random = createDeterministicRandom(11)
+
+ return Array.from(
+ { length: loaderDataRetentionNavigationCount },
+ (_, index) => `${index}-${randomSegment(random)}`,
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json
new file mode 100644
index 0000000000..953558d3f6
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-client-loader-data-retention-react",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
new file mode 100644
index 0000000000..3459edec5a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
@@ -0,0 +1,148 @@
+import type * as App from './src/app'
+
+const appModulePath = './dist/app.js'
+const { loaderPayloadRecordCount, mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+const uninitialized = () =>
+ Promise.reject(
+ new Error('loader-data-retention benchmark is not initialized'),
+ )
+
+export function setup() {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
+ )
+ }
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let expectedRenderedPath: string | undefined = undefined
+ let navigateTo: (id: string) => Promise = uninitialized
+
+ function assertRenderedShell() {
+ const actual =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+
+ if (actual !== 'shell') {
+ throw new Error(`Expected rendered shell page, got ${actual}`)
+ }
+ }
+
+ function assertRenderedPage(id: string) {
+ const page = container?.querySelector(
+ '[data-bench-page="page"]',
+ )
+ const actualId = page?.dataset.benchId
+ const actualCount = page?.dataset.benchCount
+ const expectedCount = String(loaderPayloadRecordCount)
+
+ if (actualId !== id || actualCount !== expectedCount) {
+ throw new Error(
+ `Expected rendered page ${id}:${expectedCount}, got ${actualId}:${actualCount}`,
+ )
+ }
+ }
+
+ async function waitForRenderedShell() {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedShell()
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedShell()
+ }
+
+ function waitForNextRender(pathname: string) {
+ expectedRenderedPath = pathname
+
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const { router } = mounted
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', (event) => {
+ if (
+ expectedRenderedPath &&
+ event.toLocation.pathname !== expectedRenderedPath
+ ) {
+ return
+ }
+
+ const resolve = resolveRendered
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ resolve()
+ })
+
+ navigateTo = async (id) => {
+ const pathname = `/page/${id}`
+ const rendered = waitForNextRender(pathname)
+
+ await router.navigate({
+ to: '/page/$id',
+ params: { id },
+ replace: true,
+ })
+ await rendered
+ assertRenderedPage(id)
+ }
+
+ await router.load()
+ await waitForRenderedShell()
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ navigateTo = uninitialized
+ }
+
+ return {
+ before,
+ navigate: (id: string) => navigateTo(id),
+ async sanity() {
+ await before()
+
+ try {
+ await navigateTo('sanity-a')
+ assertRenderedPage('sanity-a')
+ await navigateTo('sanity-b')
+ assertRenderedPage('sanity-b')
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx
new file mode 100644
index 0000000000..8e09296b9c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx
@@ -0,0 +1,33 @@
+import { RouterProvider } from '@tanstack/react-router'
+import { createRoot } from 'react-dom/client'
+import { getRouter } from './router'
+
+export { loaderPayloadRecordCount } from './loader-data'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const reactRoot = createRoot(container)
+ let didUnmount = false
+
+ reactRoot.render()
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ reactRoot.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts
new file mode 100644
index 0000000000..d2a73a6542
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts
@@ -0,0 +1,48 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '../../../../bench-utils'
+
+export const loaderPayloadRecordCount = 512
+
+const loaderPayloadCharsPerRecord = 1024
+
+type LoaderRecord = {
+ key: string
+ value: string
+}
+
+export function createLoaderData(id: string) {
+ const random = createDeterministicRandom(hashId(id))
+ const records: Array = []
+
+ for (let index = 0; index < loaderPayloadRecordCount; index++) {
+ records.push({
+ key: `${id}:${index}:${randomSegment(random)}`,
+ value: createRecordValue(id, index, random),
+ })
+ }
+
+ return { id, records }
+}
+
+function createRecordValue(id: string, index: number, random: () => number) {
+ const firstSegment = randomSegment(random)
+ const secondSegment = randomSegment(random)
+ const segment = `${id}:${index}:${firstSegment}:${secondSegment}:`
+
+ return segment
+ .repeat(Math.ceil(loaderPayloadCharsPerRecord / segment.length))
+ .slice(0, loaderPayloadCharsPerRecord)
+}
+
+function hashId(id: string) {
+ let hash = 2166136261
+
+ for (let index = 0; index < id.length; index++) {
+ hash ^= id.charCodeAt(index)
+ hash = Math.imul(hash, 16777619)
+ }
+
+ return hash >>> 0
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..ea96d809c8
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts
@@ -0,0 +1,102 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ShellRouteImport } from './routes/shell'
+import { Route as PageIdRouteImport } from './routes/page.$id'
+import { Route as ShellIndexRouteImport } from './routes/shell.index'
+
+const ShellRoute = ShellRouteImport.update({
+ id: '/shell',
+ path: '/shell',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PageIdRoute = PageIdRouteImport.update({
+ id: '/page/$id',
+ path: '/page/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ShellIndexRoute = ShellIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => ShellRoute,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/shell': typeof ShellRouteWithChildren
+ '/page/$id': typeof PageIdRoute
+ '/shell/': typeof ShellIndexRoute
+}
+export interface FileRoutesByTo {
+ '/page/$id': typeof PageIdRoute
+ '/shell': typeof ShellIndexRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/shell': typeof ShellRouteWithChildren
+ '/page/$id': typeof PageIdRoute
+ '/shell/': typeof ShellIndexRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/shell' | '/page/$id' | '/shell/'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/page/$id' | '/shell'
+ id: '__root__' | '/shell' | '/page/$id' | '/shell/'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ShellRoute: typeof ShellRouteWithChildren
+ PageIdRoute: typeof PageIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/shell': {
+ id: '/shell'
+ path: '/shell'
+ fullPath: '/shell'
+ preLoaderRoute: typeof ShellRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/page/$id': {
+ id: '/page/$id'
+ path: '/page/$id'
+ fullPath: '/page/$id'
+ preLoaderRoute: typeof PageIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/shell/': {
+ id: '/shell/'
+ path: '/'
+ fullPath: '/shell/'
+ preLoaderRoute: typeof ShellIndexRouteImport
+ parentRoute: typeof ShellRoute
+ }
+ }
+}
+
+interface ShellRouteChildren {
+ ShellIndexRoute: typeof ShellIndexRoute
+}
+
+const ShellRouteChildren: ShellRouteChildren = {
+ ShellIndexRoute: ShellIndexRoute,
+}
+
+const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren)
+
+const rootRouteChildren: RootRouteChildren = {
+ ShellRoute: ShellRouteWithChildren,
+ PageIdRoute: PageIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx
new file mode 100644
index 0000000000..5a5c8210d9
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx
@@ -0,0 +1,19 @@
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/shell'],
+ }),
+ routeTree,
+ defaultGcTime: 0,
+ defaultPreloadGcTime: 0,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..889395056b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx
new file mode 100644
index 0000000000..bb387a7034
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createLoaderData } from '../loader-data'
+
+export const Route = createFileRoute('/page/$id')({
+ loader: ({ params }) => createLoaderData(params.id),
+ component: PageComponent,
+})
+
+function PageComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.id}:${data.records.length}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx
new file mode 100644
index 0000000000..d78bb8bfb9
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/shell/')({
+ component: ShellIndexComponent,
+})
+
+function ShellIndexComponent() {
+ return ready
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx
new file mode 100644
index 0000000000..024a58c243
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/shell')({
+ component: ShellComponent,
+})
+
+function ShellComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json
new file mode 100644
index 0000000000..26767bd491
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts
new file mode 100644
index 0000000000..555e5f5612
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client loader-data-retention (react)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts
new file mode 100644
index 0000000000..042109dd19
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts
@@ -0,0 +1,21 @@
+import { bench, describe } from 'vitest'
+import { memoryBenchOptions } from '../../../bench-utils'
+import { setup } from './setup'
+
+const mountUnmountIterations = 100
+
+await setup().sanity()
+
+describe('memory', () => {
+ const test = setup()
+
+ bench(
+ 'mem mount-unmount (react)',
+ async () => {
+ for (let index = 0; index < mountUnmountIterations; index++) {
+ await test.cycle()
+ }
+ },
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/project.json b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json
new file mode 100644
index 0000000000..f701b92e2a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-client-mount-unmount-react",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
new file mode 100644
index 0000000000..3ba792e11a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
@@ -0,0 +1,66 @@
+import type * as App from './src/app'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+function drainMicrotasks() {
+ return Promise.resolve().then(() => Promise.resolve())
+}
+
+function assertEmptyBody() {
+ if (document.body.childNodes.length !== 0) {
+ throw new Error(
+ `Expected document.body to be empty, found ${document.body.childNodes.length} child node(s)`,
+ )
+ }
+}
+
+export function setup() {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
+ )
+ }
+
+ async function cycle() {
+ const container = document.createElement('div')
+ document.body.append(container)
+
+ let unmount = () => {}
+ let unsubscribe = () => {}
+
+ try {
+ const mounted = mountTestApp(container)
+ const { router } = mounted
+ unmount = mounted.unmount
+
+ const rendered = new Promise((resolve) => {
+ unsubscribe = router.subscribe('onRendered', () => {
+ resolve()
+ })
+ })
+
+ await router.load()
+ await rendered
+ unsubscribe()
+ unsubscribe = () => {}
+ } finally {
+ unmount()
+ container.remove()
+ unsubscribe()
+ await drainMicrotasks()
+ }
+ }
+
+ return {
+ cycle,
+ async sanity() {
+ assertEmptyBody()
+ await cycle()
+ await cycle()
+ assertEmptyBody()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx
new file mode 100644
index 0000000000..bdf76eaa24
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx
@@ -0,0 +1,29 @@
+import { RouterProvider } from '@tanstack/react-router'
+import { createRoot } from 'react-dom/client'
+import { getRouter } from './router'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const reactRoot = createRoot(container)
+ let didUnmount = false
+
+ reactRoot.render()
+
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ reactRoot.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..5efcb2cf72
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts
@@ -0,0 +1,59 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ARouteImport } from './routes/a'
+
+const ARoute = ARouteImport.update({
+ id: '/a',
+ path: '/a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/a': typeof ARoute
+}
+export interface FileRoutesByTo {
+ '/a': typeof ARoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/a': typeof ARoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/a'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/a'
+ id: '__root__' | '/a'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ARoute: typeof ARoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/a': {
+ id: '/a'
+ path: '/a'
+ fullPath: '/a'
+ preLoaderRoute: typeof ARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ARoute: ARoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx
new file mode 100644
index 0000000000..9cb85c5a1b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/a'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..889395056b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx
new file mode 100644
index 0000000000..1e268d3c42
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a')({
+ loader: () => ({ id: 'a' }),
+ component: AComponent,
+})
+
+function AComponent() {
+ const data = Route.useLoaderData()
+
+ return {data.id}
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json
new file mode 100644
index 0000000000..26767bd491
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts
new file mode 100644
index 0000000000..bb0f3c2e0a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client mount-unmount (react)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts
new file mode 100644
index 0000000000..7e2fe82027
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts
@@ -0,0 +1,34 @@
+import { afterAll, beforeAll, bench, describe } from 'vitest'
+import { memoryBenchOptions } from '../../../bench-utils'
+import { setup } from './setup'
+
+const navigationChurnIterations = 300
+
+await setup().sanity()
+
+describe('memory', () => {
+ const test = setup()
+
+ /**
+ * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
+ * honors tinybench's setup/teardown options; the CodSpeed runner does the
+ * exact opposite. Both registrations are load-bearing — exactly one pair
+ * runs in any given mode.
+ */
+ beforeAll(test.before)
+ afterAll(test.after)
+
+ bench(
+ 'mem navigation-churn (react)',
+ async () => {
+ for (let index = 0; index < navigationChurnIterations; index++) {
+ await test.navigate(index % 2 === 0 ? '/b' : '/a')
+ }
+ },
+ {
+ ...memoryBenchOptions,
+ setup: test.before,
+ teardown: test.after,
+ },
+ )
+})
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/project.json b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json
new file mode 100644
index 0000000000..1409a41b61
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-client-navigation-churn-react",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
new file mode 100644
index 0000000000..f31c63c360
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
@@ -0,0 +1,117 @@
+import type * as App from './src/app'
+
+type Target = '/a' | '/b'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+const uninitialized = () =>
+ Promise.reject(new Error('navigation-churn benchmark is not initialized'))
+
+export function setup() {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
+ )
+ }
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let navigateTo: (target: Target) => Promise = uninitialized
+
+ function assertRenderedPage(target: Target) {
+ const expected = target.slice(1)
+ const actual =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+
+ if (actual !== expected) {
+ throw new Error(`Expected rendered page ${expected}, got ${actual}`)
+ }
+ }
+
+ async function waitForRenderedPage(target: Target) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedPage(target)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedPage(target)
+ }
+
+ function waitForNextRender() {
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const { router } = mounted
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', () => {
+ resolveRendered()
+ })
+
+ navigateTo = async (target) => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router.navigate({
+ to: target,
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ await router.load()
+ await waitForRenderedPage('/a')
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ navigateTo = uninitialized
+ }
+
+ return {
+ before,
+ navigate: (target: Target) => navigateTo(target),
+ async sanity() {
+ await before()
+
+ try {
+ assertRenderedPage('/a')
+ await navigateTo('/b')
+ assertRenderedPage('/b')
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx
new file mode 100644
index 0000000000..d714e7bfdd
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx
@@ -0,0 +1,31 @@
+import { RouterProvider } from '@tanstack/react-router'
+import { createRoot } from 'react-dom/client'
+import { getRouter } from './router'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const reactRoot = createRoot(container)
+ let didUnmount = false
+
+ reactRoot.render()
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ reactRoot.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..3cca81cabb
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts
@@ -0,0 +1,77 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as BRouteImport } from './routes/b'
+import { Route as ARouteImport } from './routes/a'
+
+const BRoute = BRouteImport.update({
+ id: '/b',
+ path: '/b',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ARoute = ARouteImport.update({
+ id: '/a',
+ path: '/a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRoutesByTo {
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/a' | '/b'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/a' | '/b'
+ id: '__root__' | '/a' | '/b'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ARoute: typeof ARoute
+ BRoute: typeof BRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/b': {
+ id: '/b'
+ path: '/b'
+ fullPath: '/b'
+ preLoaderRoute: typeof BRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/a': {
+ id: '/a'
+ path: '/a'
+ fullPath: '/a'
+ preLoaderRoute: typeof ARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ARoute: ARoute,
+ BRoute: BRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx
new file mode 100644
index 0000000000..9cb85c5a1b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/a'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..889395056b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx
new file mode 100644
index 0000000000..bdd32d0b81
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+const fixedTimestamp = 1_700_000_000_000
+
+export const Route = createFileRoute('/a')({
+ loader: () => ({ name: 'a', ts: fixedTimestamp }),
+ component: AComponent,
+})
+
+function AComponent() {
+ const data = Route.useLoaderData()
+
+ return {`${data.name}:${data.ts}`}
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx
new file mode 100644
index 0000000000..05deef729d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+const fixedTimestamp = 1_700_000_000_000
+
+export const Route = createFileRoute('/b')({
+ loader: () => ({ name: 'b', ts: fixedTimestamp }),
+ component: BComponent,
+})
+
+function BComponent() {
+ const data = Route.useLoaderData()
+
+ return {`${data.name}:${data.ts}`}
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json
new file mode 100644
index 0000000000..26767bd491
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts
new file mode 100644
index 0000000000..b05b999297
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client navigation-churn (react)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts
new file mode 100644
index 0000000000..499fb8461b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts
@@ -0,0 +1,55 @@
+import { afterAll, beforeAll, bench, describe } from 'vitest'
+import {
+ createDeterministicRandom,
+ memoryBenchOptions,
+ randomSegment,
+} from '../../../bench-utils'
+import { setup } from './setup'
+
+const preloadChurnIterations = 200
+// A navigation commit is what triggers the router's clearExpiredCache —
+// preloaded matches (defaultPreloadGcTime: 0) are only evicted then, never
+// during a preload-only loop. Interleaving a navigation every few preloads is
+// what makes the flat floor assert "eviction releases preloaded payloads".
+const preloadsPerEvictionNavigation = 10
+// Module-level so ids stay unique across the CodSpeed runner's multiple
+// invocations (warmup + measured) of the bench fn on one mount; a
+// per-invocation LCG would replay identical ids, and every preload after the
+// first invocation would dedupe against cachedMatches instead of doing work.
+const benchmarkRandom = createDeterministicRandom(0x706c6f61)
+let preloadCounter = 0
+
+await setup().sanity()
+
+describe('memory', () => {
+ const test = setup()
+
+ /**
+ * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
+ * honors tinybench's setup/teardown options; the CodSpeed runner does the
+ * exact opposite. Both registrations are load-bearing — exactly one pair
+ * runs in any given mode.
+ */
+ beforeAll(test.before)
+ afterAll(test.after)
+
+ bench(
+ 'mem preload-churn (react)',
+ async () => {
+ for (let index = 0; index < preloadChurnIterations; index++) {
+ await test.preload(
+ `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`,
+ )
+
+ if ((index + 1) % preloadsPerEvictionNavigation === 0) {
+ await test.evictPreloads()
+ }
+ }
+ },
+ {
+ ...memoryBenchOptions,
+ setup: test.before,
+ teardown: test.after,
+ },
+ )
+})
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/project.json b/benchmarks/memory/client/scenarios/preload-churn/react/project.json
new file mode 100644
index 0000000000..33c837bc6c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-client-preload-churn-react",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
new file mode 100644
index 0000000000..34c2e51558
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
@@ -0,0 +1,239 @@
+import type * as App from './src/app'
+
+const appModulePath = './dist/app.js'
+const { getTrackedItemLoaderCount, mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+type MountedApp = ReturnType
+
+// Fixed id for the eviction navigations interleaved into the bench loop; its
+// payload is a constant-size steady-state resident, never part of the signal.
+const evictionItemId = 'nav-evict'
+
+const uninitialized = async (_id: string) => {
+ throw new Error('preload-churn benchmark is not initialized')
+}
+
+async function drainMicrotasks() {
+ await Promise.resolve()
+ await Promise.resolve()
+}
+
+export function setup() {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
+ )
+ }
+
+ let container: HTMLDivElement | undefined = undefined
+ let router: MountedApp['router'] | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let evictionParity = 0
+ let preloadItem: (id: string) => Promise = uninitialized
+ let navigateToItem: (id: string) => Promise = uninitialized
+ let navigateToIndex: () => Promise = () =>
+ uninitialized('navigate-to-index')
+
+ function assertRenderedIndex() {
+ const actual =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+
+ if (actual !== 'index') {
+ throw new Error(`Expected rendered index page, got ${actual}`)
+ }
+ }
+
+ function assertRenderedItem(id: string) {
+ const page =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+ const actualId =
+ container?.querySelector('[data-bench-id]')?.dataset.benchId
+
+ if (page !== 'item' || actualId !== id) {
+ throw new Error(`Expected rendered item ${id}, got ${page}:${actualId}`)
+ }
+ }
+
+ function hasCachedItemMatch(id: string) {
+ return Boolean(
+ router?.stores.cachedMatches
+ .get()
+ .some((match) => (match.params as { id?: string }).id === id),
+ )
+ }
+
+ async function waitForRenderedIndex() {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedIndex()
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedIndex()
+ }
+
+ async function waitForRenderedItem(id: string) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedItem(id)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedItem(id)
+ }
+
+ function waitForNextRender() {
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ router = mounted.router
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', () => {
+ resolveRendered()
+ })
+
+ preloadItem = async (id) => {
+ await router!.preloadRoute({
+ to: '/items/$id',
+ params: { id },
+ })
+ await drainMicrotasks()
+ }
+
+ navigateToItem = async (id) => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router!.navigate({
+ to: '/items/$id',
+ params: { id },
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ navigateToIndex = async () => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router!.navigate({
+ to: '/',
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ await router.load()
+ await waitForRenderedIndex()
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ router = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ evictionParity = 0
+ preloadItem = uninitialized
+ navigateToItem = uninitialized
+ navigateToIndex = () => uninitialized('navigate-to-index')
+ }
+
+ // Alternate between two distinct locations so every eviction navigation
+ // changes the href (a same-href navigate would never commit or render).
+ async function evictPreloads() {
+ evictionParity = (evictionParity + 1) % 2
+
+ if (evictionParity === 1) {
+ await navigateToItem(evictionItemId)
+ } else {
+ await navigateToIndex()
+ }
+ }
+
+ return {
+ before,
+ preload: (id: string) => preloadItem(id),
+ evictPreloads,
+ async sanity() {
+ await before()
+
+ try {
+ assertRenderedIndex()
+
+ const id = 'sanity-preloaded-item'
+ const initialLoaderCount = getTrackedItemLoaderCount(id)
+ await preloadItem(id)
+
+ const preloadedLoaderCount = getTrackedItemLoaderCount(id)
+ if (preloadedLoaderCount !== initialLoaderCount + 1) {
+ throw new Error(
+ `Expected preload to run item loader once, got ${preloadedLoaderCount - initialLoaderCount}`,
+ )
+ }
+
+ if (!hasCachedItemMatch(id)) {
+ throw new Error(
+ 'Expected preloaded match to sit in router.state.cachedMatches',
+ )
+ }
+
+ // A navigation commit runs clearExpiredCache; with
+ // defaultPreloadGcTime: 0 it must evict the preloaded match. This is
+ // the mechanism the bench's flat-floor expectation rests on.
+ await navigateToItem('sanity-evict-nav')
+ await waitForRenderedItem('sanity-evict-nav')
+
+ if (hasCachedItemMatch(id)) {
+ throw new Error(
+ 'Expected the navigation commit to evict the preloaded match (preloadGcTime 0)',
+ )
+ }
+
+ await preloadItem(id)
+
+ const repreloadedLoaderCount = getTrackedItemLoaderCount(id)
+ if (repreloadedLoaderCount !== preloadedLoaderCount + 1) {
+ throw new Error(
+ 'Expected re-preload after eviction to run the item loader again',
+ )
+ }
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx
new file mode 100644
index 0000000000..8e60bad71d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx
@@ -0,0 +1,33 @@
+import { RouterProvider } from '@tanstack/react-router'
+import { createRoot } from 'react-dom/client'
+import { getRouter } from './router'
+
+export { getTrackedItemLoaderCount } from './item-payload'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const reactRoot = createRoot(container)
+ let didUnmount = false
+
+ reactRoot.render()
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ reactRoot.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts
new file mode 100644
index 0000000000..4a86ea7244
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts
@@ -0,0 +1,51 @@
+const payloadByteLength = 20 * 1024
+const payloadChunkCount = 32
+const payloadChunkSize = payloadByteLength / payloadChunkCount
+const trackedLoaderIdPrefix = 'sanity-'
+const trackedItemLoaderCalls = new Map()
+
+export function createItemPayload(id: string) {
+ let seed = hashId(id)
+ const chunks = new Array(payloadChunkCount)
+
+ for (let index = 0; index < payloadChunkCount; index++) {
+ seed = (seed * 1664525 + 1013904223) >>> 0
+ chunks[index] = createPayloadChunk(id, index, seed)
+ }
+
+ return {
+ id,
+ chunks,
+ byteLength: payloadByteLength,
+ }
+}
+
+export function trackItemLoaderCall(id: string) {
+ if (!id.startsWith(trackedLoaderIdPrefix)) {
+ return
+ }
+
+ trackedItemLoaderCalls.set(id, (trackedItemLoaderCalls.get(id) ?? 0) + 1)
+}
+
+export function getTrackedItemLoaderCount(id: string) {
+ return trackedItemLoaderCalls.get(id) ?? 0
+}
+
+function createPayloadChunk(id: string, index: number, seed: number) {
+ const token = `${id}:${index.toString(36)}:${seed.toString(36)}:`
+ const repeatCount = Math.ceil(payloadChunkSize / token.length)
+
+ return token.repeat(repeatCount).slice(0, payloadChunkSize)
+}
+
+function hashId(id: string) {
+ let hash = 2166136261
+
+ for (let index = 0; index < id.length; index++) {
+ hash ^= id.charCodeAt(index)
+ hash = Math.imul(hash, 16777619)
+ }
+
+ return hash >>> 0
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..84aaebb9c9
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts
@@ -0,0 +1,77 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/items/$id'
+ id: '__root__' | '/' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx
new file mode 100644
index 0000000000..69e3180173
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx
@@ -0,0 +1,18 @@
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree,
+ defaultPreloadGcTime: 0,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..889395056b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx
new file mode 100644
index 0000000000..af735d705b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return index
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx
new file mode 100644
index 0000000000..36c8579ddf
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createItemPayload, trackItemLoaderCall } from '../item-payload'
+
+export const Route = createFileRoute('/items/$id')({
+ loader: ({ params }) => {
+ trackItemLoaderCall(params.id)
+ return createItemPayload(params.id)
+ },
+ component: ItemComponent,
+})
+
+function ItemComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.id}:${data.byteLength}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json
new file mode 100644
index 0000000000..26767bd491
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts
new file mode 100644
index 0000000000..f9c426656b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client preload-churn (react)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts
new file mode 100644
index 0000000000..db56f30d8d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts
@@ -0,0 +1,46 @@
+import { afterAll, beforeAll, bench, describe } from 'vitest'
+import {
+ createDeterministicRandom,
+ memoryBenchOptions,
+ randomSegment,
+} from '../../../bench-utils'
+import { setup } from './setup'
+
+const benchmarkRandom = createDeterministicRandom(0xdecafbad)
+const uniqueLocationChurnIterations = 300
+// Module-level so ids stay unique across the CodSpeed runner's multiple
+// invocations (warmup + measured) of the bench fn on one mount; the counter
+// prefix removes any residual LCG birthday-collision risk.
+let locationCounter = 0
+
+await setup().sanity()
+
+describe('memory', () => {
+ const test = setup()
+
+ /**
+ * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
+ * honors tinybench's setup/teardown options; the CodSpeed runner does the
+ * exact opposite. Both registrations are load-bearing — exactly one pair
+ * runs in any given mode.
+ */
+ beforeAll(test.before)
+ afterAll(test.after)
+
+ bench(
+ 'mem unique-location-churn (react)',
+ async () => {
+ for (let index = 0; index < uniqueLocationChurnIterations; index++) {
+ const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`
+ const q = `q-${randomSegment(benchmarkRandom)}`
+
+ await test.navigate({ id, q })
+ }
+ },
+ {
+ ...memoryBenchOptions,
+ setup: test.before,
+ teardown: test.after,
+ },
+ )
+})
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json
new file mode 100644
index 0000000000..db7c57b1a3
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-client-unique-location-churn-react",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
new file mode 100644
index 0000000000..6f58a1281a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
@@ -0,0 +1,123 @@
+import type * as App from './src/app'
+
+type ItemLocation = {
+ id: string
+ q: string
+}
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+const uninitialized = () =>
+ Promise.reject(
+ new Error('unique-location-churn benchmark is not initialized'),
+ )
+
+export function setup() {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
+ )
+ }
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let navigateTo: (location: ItemLocation) => Promise = uninitialized
+
+ function assertRenderedId(expected: string) {
+ const actual =
+ container?.querySelector('[data-bench-id]')?.dataset.benchId
+
+ if (actual !== expected) {
+ throw new Error(`Expected rendered item id ${expected}, got ${actual}`)
+ }
+ }
+
+ async function waitForRenderedId(expected: string) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedId(expected)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedId(expected)
+ }
+
+ function waitForNextRender() {
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const { router } = mounted
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', () => {
+ resolveRendered()
+ })
+
+ navigateTo = async (location) => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router.navigate({
+ to: '/items/$id',
+ params: { id: location.id },
+ search: { q: location.q },
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ await router.load()
+ await waitForRenderedId('initial')
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ navigateTo = uninitialized
+ }
+
+ return {
+ before,
+ navigate: (location: ItemLocation) => navigateTo(location),
+ async sanity() {
+ await before()
+
+ try {
+ await navigateTo({ id: 'sanity-one', q: 'q-sanity-one' })
+ assertRenderedId('sanity-one')
+ await navigateTo({ id: 'sanity-two', q: 'q-sanity-two' })
+ assertRenderedId('sanity-two')
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx
new file mode 100644
index 0000000000..d714e7bfdd
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx
@@ -0,0 +1,31 @@
+import { RouterProvider } from '@tanstack/react-router'
+import { createRoot } from 'react-dom/client'
+import { getRouter } from './router'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const reactRoot = createRoot(container)
+ let didUnmount = false
+
+ reactRoot.render()
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ reactRoot.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..1165f57523
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts
@@ -0,0 +1,59 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/items/$id'
+ id: '__root__' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx
new file mode 100644
index 0000000000..50ad269060
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/items/initial?q=q-initial'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..889395056b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx
new file mode 100644
index 0000000000..c9e823f6c2
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+type ItemSearch = {
+ q: string
+}
+
+export const Route = createFileRoute('/items/$id')({
+ validateSearch: (search: Record): ItemSearch => ({
+ q: typeof search.q === 'string' ? search.q : '',
+ }),
+ loaderDeps: ({ search }) => ({ q: search.q }),
+ loader: ({ params, deps }) => ({
+ id: params.id,
+ q: deps.q,
+ checksum: params.id.length + deps.q.length,
+ }),
+ component: ItemComponent,
+})
+
+function ItemComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+ {`${data.id}:${data.q}:${data.checksum}`}
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json
new file mode 100644
index 0000000000..26767bd491
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts
new file mode 100644
index 0000000000..f424c57cef
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client unique-location-churn (react)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/tsconfig.json b/benchmarks/memory/client/tsconfig.json
new file mode 100644
index 0000000000..5350368145
--- /dev/null
+++ b/benchmarks/memory/client/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": ["bench-utils.ts", "vitest.setup.ts"]
+}
diff --git a/benchmarks/memory/client/vitest.react.config.ts b/benchmarks/memory/client/vitest.react.config.ts
new file mode 100644
index 0000000000..356ab7b3a8
--- /dev/null
+++ b/benchmarks/memory/client/vitest.react.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ watch: false,
+ projects: ['./scenarios/*/react/vite.config.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/vitest.setup.ts b/benchmarks/memory/client/vitest.setup.ts
new file mode 100644
index 0000000000..9bac58bd41
--- /dev/null
+++ b/benchmarks/memory/client/vitest.setup.ts
@@ -0,0 +1,6 @@
+import { vi } from 'vitest'
+
+// @ts-expect-error
+global.IS_REACT_ACT_ENVIRONMENT = true
+
+window.scrollTo = vi.fn()
diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts
new file mode 100644
index 0000000000..2613d81828
--- /dev/null
+++ b/benchmarks/memory/server/bench-utils.ts
@@ -0,0 +1,63 @@
+export interface StartRequestHandler {
+ fetch: (request: Request) => Promise | Response
+}
+
+export interface RunSequentialRequestLoopOptions {
+ seed: number
+ iterations?: number
+ buildRequest: (random: () => number, index: number) => Request
+ validateResponse?: (response: Response, request: Request) => void
+ validateBody?: (body: string, response: Response, request: Request) => void
+}
+
+export const memoryBenchOptions = {
+ iterations: 1,
+ warmupIterations: 1,
+ time: 0,
+ warmupTime: 0,
+}
+
+export function createDeterministicRandom(seed: number) {
+ let state = seed >>> 0
+
+ return () => {
+ state = (state * 1664525 + 1013904223) >>> 0
+ return state / 0x100000000
+ }
+}
+
+export function randomSegment(random: () => number) {
+ return Math.floor(random() * 1_000_000_000).toString(36)
+}
+
+export async function runSequentialRequestLoop(
+ handler: StartRequestHandler,
+ {
+ seed,
+ iterations = 10,
+ buildRequest,
+ validateResponse,
+ validateBody,
+ }: RunSequentialRequestLoopOptions,
+) {
+ const random = createDeterministicRandom(seed)
+ const validate =
+ validateResponse ??
+ ((response: Response, request: Request) => {
+ if (response.status !== 200) {
+ throw new Error(
+ `Request failed with non-200 status ${response.status} (${request.url})`,
+ )
+ }
+ })
+
+ for (let index = 0; index < iterations; index++) {
+ const request = buildRequest(random, index)
+ const response = await handler.fetch(request)
+
+ validate(response, request)
+
+ const body = await response.text()
+ validateBody?.(body, response, request)
+ }
+}
diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json
new file mode 100644
index 0000000000..5e4273c30b
--- /dev/null
+++ b/benchmarks/memory/server/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "@benchmarks/memory-server",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@codspeed/vitest-plugin": "^5.5.0",
+ "@vitejs/plugin-react": "^6.0.1",
+ "typescript": "^6.0.2",
+ "vite": "^8.0.14",
+ "vitest": "^4.1.4"
+ },
+ "nx": {
+ "targets": {
+ "build:react": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-react",
+ "@benchmarks/memory-server-server-fn-churn-react",
+ "@benchmarks/memory-server-error-paths-react",
+ "@benchmarks/memory-server-aborted-requests-react",
+ "@benchmarks/memory-server-peak-large-page-react",
+ "@benchmarks/memory-server-streaming-peak-react",
+ "@benchmarks/memory-server-serialization-payload-react"
+ ],
+ "target": "build:ssr"
+ }
+ ]
+ },
+ "test:perf:react": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ "build:react"
+ ],
+ "options": {
+ "command": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts",
+ "cwd": "benchmarks/memory/server"
+ }
+ },
+ "test:types": {
+ "executor": "nx:noop",
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-react",
+ "@benchmarks/memory-server-server-fn-churn-react",
+ "@benchmarks/memory-server-error-paths-react",
+ "@benchmarks/memory-server-aborted-requests-react",
+ "@benchmarks/memory-server-peak-large-page-react",
+ "@benchmarks/memory-server-streaming-peak-react",
+ "@benchmarks/memory-server-serialization-payload-react"
+ ],
+ "target": "test:types:ssr"
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts
new file mode 100644
index 0000000000..b800cb974f
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts
@@ -0,0 +1,160 @@
+import { bench, describe } from 'vitest'
+import { memoryBenchOptions } from '../../../bench-utils'
+import type { StartRequestHandler } from '../../../bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const abortedRequestIterations = 100
+const eagerMarker = 'data-bench="aborted-requests-eager"'
+const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"'
+const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"'
+const alphaFirstRecord = (id: string) => `deferred-alpha-${id}-0`
+const alphaLastRecord = (id: string) => `deferred-alpha-${id}-19`
+const betaFirstRecord = (id: string) => `deferred-beta-${id}-0`
+const betaLastRecord = (id: string) => `deferred-beta-${id}-19`
+
+const textDecoder = new TextDecoder()
+const documentRequestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+)) as {
+ default: StartRequestHandler
+}
+
+function buildStreamRequest(id: string, signal?: AbortSignal) {
+ const init: RequestInit = { ...documentRequestInit }
+
+ if (signal) {
+ init.signal = signal
+ }
+
+ return new Request(`http://localhost/stream/${id}`, init)
+}
+
+function validateDocumentResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+async function readFirstChunk(response: Response, request: Request) {
+ if (!response.body) {
+ throw new Error(`Expected response body for ${request.url}`)
+ }
+
+ const reader = response.body.getReader()
+ const result = await reader.read()
+ const value = result.value
+
+ if (result.done || !value || value.byteLength === 0) {
+ await reader.cancel()
+ throw new Error(`Expected a non-empty first chunk for ${request.url}`)
+ }
+
+ return {
+ reader,
+ value,
+ }
+}
+
+async function drainCancellationMicrotasks() {
+ await Promise.resolve()
+ await Promise.resolve()
+}
+
+async function assertAbortedRequestsSanity(handler: StartRequestHandler) {
+ const fullId = 'sanity-full'
+ const fullRequest = buildStreamRequest(fullId)
+ const fullResponse = await handler.fetch(fullRequest)
+ validateDocumentResponse(fullResponse, fullRequest)
+
+ const fullBody = await fullResponse.text()
+
+ if (!fullBody.includes(eagerMarker)) {
+ throw new Error('Expected full sanity response to include the eager marker')
+ }
+
+ for (const marker of [
+ alphaFirstRecord(fullId),
+ alphaLastRecord(fullId),
+ betaFirstRecord(fullId),
+ betaLastRecord(fullId),
+ ]) {
+ if (!fullBody.includes(marker)) {
+ throw new Error(`Expected full sanity response to include ${marker}`)
+ }
+ }
+
+ const midStreamId = 'sanity-mid-stream'
+ const controller = new AbortController()
+ const midStreamRequest = buildStreamRequest(midStreamId, controller.signal)
+ const midStreamResponse = await handler.fetch(midStreamRequest)
+ validateDocumentResponse(midStreamResponse, midStreamRequest)
+
+ const { reader, value } = await readFirstChunk(
+ midStreamResponse,
+ midStreamRequest,
+ )
+ const text = textDecoder.decode(value)
+
+ if (!text.includes(eagerMarker)) {
+ throw new Error('Expected first sanity chunk to include the eager marker')
+ }
+
+ if (
+ !text.includes(alphaFallbackMarker) ||
+ !text.includes(betaFallbackMarker)
+ ) {
+ throw new Error('Expected first sanity chunk to include deferred fallbacks')
+ }
+
+ for (const marker of [
+ alphaFirstRecord(midStreamId),
+ alphaLastRecord(midStreamId),
+ betaFirstRecord(midStreamId),
+ betaLastRecord(midStreamId),
+ ]) {
+ if (text.includes(marker)) {
+ throw new Error(
+ `First sanity chunk already included deferred content ${marker}`,
+ )
+ }
+ }
+
+ // reader.cancel() is the response-stream cancellation path if the handler
+ // does not observe Request.signal for this in-process request.
+ controller.abort()
+ await reader.cancel()
+ await drainCancellationMicrotasks()
+}
+
+async function runAbortedRequestLoop(handler: StartRequestHandler) {
+ for (let index = 0; index < abortedRequestIterations; index++) {
+ const controller = new AbortController()
+ const request = buildStreamRequest(`abort-${index}`, controller.signal)
+ const response = await handler.fetch(request)
+ validateDocumentResponse(response, request)
+
+ const { reader } = await readFirstChunk(response, request)
+ controller.abort()
+ await reader.cancel()
+ await drainCancellationMicrotasks()
+ }
+}
+
+await assertAbortedRequestsSanity(handler)
+
+describe('memory', () => {
+ bench(
+ 'mem aborted-requests (react)',
+ () => runAbortedRequestLoop(handler),
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/project.json b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json
new file mode 100644
index 0000000000..c962404c09
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-server-aborted-requests-react",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..fbbc25ecfb
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as StreamIdRouteImport } from './routes/stream.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const StreamIdRoute = StreamIdRouteImport.update({
+ id: '/stream/$id',
+ path: '/stream/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/stream/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/stream/$id'
+ id: '__root__' | '/' | '/stream/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StreamIdRoute: typeof StreamIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/stream/$id': {
+ id: '/stream/$id'
+ path: '/stream/$id'
+ fullPath: '/stream/$id'
+ preLoaderRoute: typeof StreamIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StreamIdRoute: StreamIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx
new file mode 100644
index 0000000000..7c4eb0babe
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..c5f9de6922
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx
@@ -0,0 +1,27 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx
new file mode 100644
index 0000000000..08d534a779
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return aborted-requests-index
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx
new file mode 100644
index 0000000000..8772428338
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx
@@ -0,0 +1,77 @@
+import { Await, createFileRoute } from '@tanstack/react-router'
+import { Suspense } from 'react'
+
+const recordCount = 20
+
+type RecordGroup = 'alpha' | 'beta'
+
+export interface DeferredRecord {
+ id: string
+ label: string
+}
+
+function resolveAfterMicrotasks(microtasks: number, value: () => T) {
+ let promise = Promise.resolve()
+
+ for (let index = 0; index < microtasks; index++) {
+ promise = promise.then(() => undefined)
+ }
+
+ return promise.then(value)
+}
+
+function makeRecords(id: string, group: RecordGroup): Array {
+ return Array.from({ length: recordCount }, (_, index) => ({
+ id: `${group}-${id}-${index}`,
+ label: `deferred-${group}-${id}-${index}`,
+ }))
+}
+
+export const Route = createFileRoute('/stream/$id')({
+ loader: ({ params }) => ({
+ eager: `eager-${params.id}`,
+ alpha: resolveAfterMicrotasks(32, () => makeRecords(params.id, 'alpha')),
+ beta: resolveAfterMicrotasks(64, () => makeRecords(params.id, 'beta')),
+ }),
+ component: StreamComponent,
+})
+
+function StreamComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {data.eager}
+ loading-alpha
+ }
+ >
+
+ {(records) => (
+
+ {records.map((record) => (
+ - {record.label}
+ ))}
+
+ )}
+
+
+ loading-beta
+ }
+ >
+
+ {(records) => (
+
+ {records.map((record) => (
+ - {record.label}
+ ))}
+
+ )}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json
new file mode 100644
index 0000000000..4d78884cea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts
new file mode 100644
index 0000000000..a3f4836091
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server aborted-requests (react)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
new file mode 100644
index 0000000000..32b536881b
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
@@ -0,0 +1,201 @@
+import { bench, describe } from 'vitest'
+import {
+ memoryBenchOptions,
+ randomSegment,
+ runSequentialRequestLoop,
+} from '../../../bench-utils'
+import type { StartRequestHandler } from '../../../bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const errorPathsIterations = 50
+const redirectSeed = 0xdecafbad
+const notFoundSeed = 0xdecafb0d
+const errorSeed = 0xdecafbed
+const unmatchedSeed = 0xdecaf00d
+
+const redirectStatus = 302
+const notFoundStatus = 404
+const errorStatus = 500
+const notFoundMarker = 'data-bench="not-found-boundary"'
+const errorMarker = 'data-bench="error-boundary"'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+)) as {
+ default: StartRequestHandler
+}
+
+function buildRedirectRequest(random: () => number) {
+ return new Request(
+ `http://localhost/from/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function buildNotFoundRequest(random: () => number) {
+ return new Request(
+ `http://localhost/missing/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function buildErrorRequest(random: () => number) {
+ return new Request(
+ `http://localhost/boom/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function buildUnmatchedRequest(random: () => number) {
+ return new Request(
+ `http://localhost/nope/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function getRequestId(request: Request) {
+ const id = new URL(request.url).pathname.split('/').pop()
+
+ if (!id) {
+ throw new Error(`Expected request id in ${request.url}`)
+ }
+
+ return id
+}
+
+function validateRedirectResponse(response: Response, request: Request) {
+ if (response.status !== redirectStatus) {
+ throw new Error(
+ `Expected status ${redirectStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+
+ const id = getRequestId(request)
+ const location = response.headers.get('location')
+
+ if (location !== `/target/${id}`) {
+ throw new Error(`Expected redirect location /target/${id}, got ${location}`)
+ }
+}
+
+function validateNotFoundResponse(response: Response, request: Request) {
+ if (response.status !== notFoundStatus) {
+ throw new Error(
+ `Expected status ${notFoundStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateErrorResponse(response: Response, request: Request) {
+ if (response.status !== errorStatus) {
+ throw new Error(
+ `Expected status ${errorStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateNotFoundBody(body: string) {
+ if (!body.includes(notFoundMarker)) {
+ throw new Error('Expected error-paths not-found marker in response body')
+ }
+}
+
+function validateErrorBody(body: string) {
+ if (!body.includes(errorMarker)) {
+ throw new Error('Expected error-paths error marker in response body')
+ }
+}
+
+async function assertStatusSanity(
+ request: Request,
+ validateResponse: (response: Response, request: Request) => void,
+ validateBody?: (body: string) => void,
+) {
+ const response = await handler.fetch(request)
+ validateResponse(response, request)
+
+ const body = await response.text()
+ validateBody?.(body)
+}
+
+async function assertErrorPathsSanity() {
+ await assertStatusSanity(
+ new Request('http://localhost/from/sanity-redirect', requestInit),
+ validateRedirectResponse,
+ )
+ await assertStatusSanity(
+ new Request('http://localhost/missing/sanity-missing', requestInit),
+ validateNotFoundResponse,
+ validateNotFoundBody,
+ )
+ await assertStatusSanity(
+ new Request('http://localhost/boom/sanity-error', requestInit),
+ validateErrorResponse,
+ validateErrorBody,
+ )
+ await assertStatusSanity(
+ new Request('http://localhost/nope/sanity-unmatched', requestInit),
+ validateNotFoundResponse,
+ )
+}
+
+await assertErrorPathsSanity()
+
+describe('memory', () => {
+ bench(
+ 'mem error-paths redirect (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: redirectSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildRedirectRequest,
+ validateResponse: validateRedirectResponse,
+ }),
+ memoryBenchOptions,
+ )
+
+ bench(
+ 'mem error-paths not-found (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: notFoundSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildNotFoundRequest,
+ validateResponse: validateNotFoundResponse,
+ validateBody: validateNotFoundBody,
+ }),
+ memoryBenchOptions,
+ )
+
+ bench(
+ 'mem error-paths error (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: errorSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildErrorRequest,
+ validateResponse: validateErrorResponse,
+ validateBody: validateErrorBody,
+ }),
+ memoryBenchOptions,
+ )
+
+ bench(
+ 'mem error-paths unmatched (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: unmatchedSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildUnmatchedRequest,
+ validateResponse: validateNotFoundResponse,
+ }),
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/project.json b/benchmarks/memory/server/scenarios/error-paths/react/project.json
new file mode 100644
index 0000000000..710048160f
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-server-error-paths-react",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..fd373be6b4
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts
@@ -0,0 +1,146 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as TargetIdRouteImport } from './routes/target.$id'
+import { Route as MissingIdRouteImport } from './routes/missing.$id'
+import { Route as FromIdRouteImport } from './routes/from.$id'
+import { Route as BoomIdRouteImport } from './routes/boom.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const TargetIdRoute = TargetIdRouteImport.update({
+ id: '/target/$id',
+ path: '/target/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MissingIdRoute = MissingIdRouteImport.update({
+ id: '/missing/$id',
+ path: '/missing/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FromIdRoute = FromIdRouteImport.update({
+ id: '/from/$id',
+ path: '/from/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const BoomIdRoute = BoomIdRouteImport.update({
+ id: '/boom/$id',
+ path: '/boom/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id'
+ id:
+ | '__root__'
+ | '/'
+ | '/boom/$id'
+ | '/from/$id'
+ | '/missing/$id'
+ | '/target/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ BoomIdRoute: typeof BoomIdRoute
+ FromIdRoute: typeof FromIdRoute
+ MissingIdRoute: typeof MissingIdRoute
+ TargetIdRoute: typeof TargetIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/target/$id': {
+ id: '/target/$id'
+ path: '/target/$id'
+ fullPath: '/target/$id'
+ preLoaderRoute: typeof TargetIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/missing/$id': {
+ id: '/missing/$id'
+ path: '/missing/$id'
+ fullPath: '/missing/$id'
+ preLoaderRoute: typeof MissingIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/from/$id': {
+ id: '/from/$id'
+ path: '/from/$id'
+ fullPath: '/from/$id'
+ preLoaderRoute: typeof FromIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/boom/$id': {
+ id: '/boom/$id'
+ path: '/boom/$id'
+ fullPath: '/boom/$id'
+ preLoaderRoute: typeof BoomIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ BoomIdRoute: BoomIdRoute,
+ FromIdRoute: FromIdRoute,
+ MissingIdRoute: MissingIdRoute,
+ TargetIdRoute: TargetIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx
new file mode 100644
index 0000000000..e31bd0abf7
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx
@@ -0,0 +1,23 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ defaultNotFoundComponent: DefaultNotFound,
+ })
+}
+
+// Without this, every unmatched-URL request logs a router warning in
+// non-production ad-hoc runs.
+function DefaultNotFound() {
+ return error-paths-unmatched
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..c5f9de6922
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx
@@ -0,0 +1,27 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx
new file mode 100644
index 0000000000..fafa81a9bb
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/boom/$id')({
+ loader: ({ params }) => {
+ throw new Error(`boom-${params.id}`)
+ },
+ errorComponent: BoomErrorComponent,
+ component: BoomComponent,
+})
+
+function BoomErrorComponent() {
+ return error-paths-error-boundary
+}
+
+function BoomComponent() {
+ return null
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx
new file mode 100644
index 0000000000..4c45e76c92
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/from/$id')({
+ loader: ({ params }) => {
+ const { id } = params
+
+ throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 })
+ },
+ component: FromComponent,
+})
+
+function FromComponent() {
+ return null
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx
new file mode 100644
index 0000000000..c5492486b4
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return error-paths-index
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx
new file mode 100644
index 0000000000..84640e9712
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute, notFound } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/missing/$id')({
+ loader: () => {
+ throw notFound()
+ },
+ notFoundComponent: MissingNotFoundComponent,
+ component: MissingComponent,
+})
+
+function MissingNotFoundComponent() {
+ return (
+ error-paths-not-found-boundary
+ )
+}
+
+function MissingComponent() {
+ return null
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx
new file mode 100644
index 0000000000..1c13394cb2
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/target/$id')({
+ component: TargetComponent,
+})
+
+function TargetComponent() {
+ const params = Route.useParams()
+
+ return {`target-${params.id}`}
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json
new file mode 100644
index 0000000000..4d78884cea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts
new file mode 100644
index 0000000000..2a4fd9818a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server error-paths (react)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
new file mode 100644
index 0000000000..c1809ffd98
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
@@ -0,0 +1,76 @@
+import { bench, describe } from 'vitest'
+import {
+ memoryBenchOptions,
+ runSequentialRequestLoop,
+} from '../../../bench-utils'
+import type { StartRequestHandler } from '../../../bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0x5eed_0005
+const peakLargePageIterations = 2
+const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8'
+const levelEightMarker = 'data-bench="peak-large-page-level-8"'
+const knownDehydratedRecordName = 'peak-large-page-l8-record-199'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+)) as {
+ default: StartRequestHandler
+}
+
+function buildPeakLargePageRequest() {
+ return new Request(peakLargePageUrl, requestInit)
+}
+
+function validatePeakLargePageResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validatePeakLargePageBody(body: string) {
+ if (!body.includes(levelEightMarker)) {
+ throw new Error('Expected peak-large-page level-8 marker in response body')
+ }
+
+ if (!body.includes(knownDehydratedRecordName)) {
+ throw new Error(
+ 'Expected peak-large-page dehydrated record in response body',
+ )
+ }
+}
+
+async function assertPeakLargePageSanity(handler: StartRequestHandler) {
+ const request = new Request(peakLargePageUrl, requestInit)
+ const response = await handler.fetch(request)
+ const body = await response.text()
+
+ validatePeakLargePageResponse(response, request)
+ validatePeakLargePageBody(body)
+}
+
+await assertPeakLargePageSanity(handler)
+
+describe('memory', () => {
+ bench(
+ 'mem peak-large-page (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: peakLargePageIterations,
+ buildRequest: buildPeakLargePageRequest,
+ validateResponse: validatePeakLargePageResponse,
+ validateBody: validatePeakLargePageBody,
+ }),
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/project.json b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json
new file mode 100644
index 0000000000..89a5865a74
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-server-peak-large-page-react",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts
new file mode 100644
index 0000000000..a0cd3b4975
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts
@@ -0,0 +1,111 @@
+export interface LargePageRecord {
+ id: string
+ name: string
+ description: string
+}
+
+export interface LargePageLevelData {
+ level: number
+ marker: string
+ records: Array
+}
+
+const recordCount = 200
+const descriptionLength = 104
+
+export function makeLargePageLevelData(level: number, seed: number) {
+ const random = createDeterministicRandom(seed)
+
+ return {
+ level,
+ marker: makeLargePageMarker(level),
+ records: Array.from({ length: recordCount }, (_, index) => {
+ const idToken = randomSegment(random)
+ const descriptionTokenA = randomSegment(random)
+ const descriptionTokenB = randomSegment(random)
+
+ return {
+ id: `l${level}-${index}-${idToken}`,
+ name: makeLargePageRecordName(level, index),
+ description: makeDescription(
+ level,
+ index,
+ descriptionTokenA,
+ descriptionTokenB,
+ ),
+ }
+ }),
+ } satisfies LargePageLevelData
+}
+
+export function makeLargePageHead(loaderData: LargePageLevelData | undefined) {
+ if (!loaderData) {
+ return {
+ meta: [{ title: 'Peak Large Page' }],
+ }
+ }
+
+ const first = loaderData.records[0]!
+ const last = loaderData.records[loaderData.records.length - 1]!
+
+ return {
+ meta: [
+ { title: `Peak Large Page L${loaderData.level} ${first.name}` },
+ {
+ name: `peak-large-page-l${loaderData.level}-count`,
+ content: String(loaderData.records.length),
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-first-id`,
+ content: first.id,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-first-name`,
+ content: first.name,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-last-id`,
+ content: last.id,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-description`,
+ content: first.description,
+ },
+ ],
+ }
+}
+
+function makeLargePageRecordName(level: number, index: number) {
+ return `peak-large-page-l${level}-record-${index}`
+}
+
+function makeLargePageMarker(level: number) {
+ return `peak-large-page-level-${level}`
+}
+
+function makeDescription(
+ level: number,
+ index: number,
+ tokenA: string,
+ tokenB: string,
+) {
+ return `Level ${level} record ${index} uses seeded fragments ${tokenA} and ${tokenB} to keep a fresh deterministic loader payload.`.padEnd(
+ descriptionLength,
+ 'x',
+ )
+}
+
+// Local copy of the LCG from benchmarks/memory/server/bench-utils.ts — app
+// source cannot import from outside the app root. Keep in sync.
+function createDeterministicRandom(seed: number) {
+ let state = seed >>> 0
+
+ return () => {
+ state = (state * 1664525 + 1013904223) >>> 0
+ return state / 0x100000000
+ }
+}
+
+function randomSegment(random: () => number) {
+ return Math.floor(random() * 1_000_000_000).toString(36)
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..60a1bd1fbc
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts
@@ -0,0 +1,305 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as L1RouteImport } from './routes/l1'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as L1L2RouteImport } from './routes/l1.l2'
+import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3'
+import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4'
+import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5'
+import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6'
+import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7'
+import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8'
+
+const L1Route = L1RouteImport.update({
+ id: '/l1',
+ path: '/l1',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const L1L2Route = L1L2RouteImport.update({
+ id: '/l2',
+ path: '/l2',
+ getParentRoute: () => L1Route,
+} as any)
+const L1L2L3Route = L1L2L3RouteImport.update({
+ id: '/l3',
+ path: '/l3',
+ getParentRoute: () => L1L2Route,
+} as any)
+const L1L2L3L4Route = L1L2L3L4RouteImport.update({
+ id: '/l4',
+ path: '/l4',
+ getParentRoute: () => L1L2L3Route,
+} as any)
+const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({
+ id: '/l5',
+ path: '/l5',
+ getParentRoute: () => L1L2L3L4Route,
+} as any)
+const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({
+ id: '/l6',
+ path: '/l6',
+ getParentRoute: () => L1L2L3L4L5Route,
+} as any)
+const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({
+ id: '/l7',
+ path: '/l7',
+ getParentRoute: () => L1L2L3L4L5L6Route,
+} as any)
+const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({
+ id: '/l8',
+ path: '/l8',
+ getParentRoute: () => L1L2L3L4L5L6L7Route,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ id:
+ | '__root__'
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ L1Route: typeof L1RouteWithChildren
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/l1': {
+ id: '/l1'
+ path: '/l1'
+ fullPath: '/l1'
+ preLoaderRoute: typeof L1RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/l1/l2': {
+ id: '/l1/l2'
+ path: '/l2'
+ fullPath: '/l1/l2'
+ preLoaderRoute: typeof L1L2RouteImport
+ parentRoute: typeof L1Route
+ }
+ '/l1/l2/l3': {
+ id: '/l1/l2/l3'
+ path: '/l3'
+ fullPath: '/l1/l2/l3'
+ preLoaderRoute: typeof L1L2L3RouteImport
+ parentRoute: typeof L1L2Route
+ }
+ '/l1/l2/l3/l4': {
+ id: '/l1/l2/l3/l4'
+ path: '/l4'
+ fullPath: '/l1/l2/l3/l4'
+ preLoaderRoute: typeof L1L2L3L4RouteImport
+ parentRoute: typeof L1L2L3Route
+ }
+ '/l1/l2/l3/l4/l5': {
+ id: '/l1/l2/l3/l4/l5'
+ path: '/l5'
+ fullPath: '/l1/l2/l3/l4/l5'
+ preLoaderRoute: typeof L1L2L3L4L5RouteImport
+ parentRoute: typeof L1L2L3L4Route
+ }
+ '/l1/l2/l3/l4/l5/l6': {
+ id: '/l1/l2/l3/l4/l5/l6'
+ path: '/l6'
+ fullPath: '/l1/l2/l3/l4/l5/l6'
+ preLoaderRoute: typeof L1L2L3L4L5L6RouteImport
+ parentRoute: typeof L1L2L3L4L5Route
+ }
+ '/l1/l2/l3/l4/l5/l6/l7': {
+ id: '/l1/l2/l3/l4/l5/l6/l7'
+ path: '/l7'
+ fullPath: '/l1/l2/l3/l4/l5/l6/l7'
+ preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport
+ parentRoute: typeof L1L2L3L4L5L6Route
+ }
+ '/l1/l2/l3/l4/l5/l6/l7/l8': {
+ id: '/l1/l2/l3/l4/l5/l6/l7/l8'
+ path: '/l8'
+ fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8'
+ preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport
+ parentRoute: typeof L1L2L3L4L5L6L7Route
+ }
+ }
+}
+
+interface L1L2L3L4L5L6L7RouteChildren {
+ L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route
+}
+
+const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = {
+ L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route,
+}
+
+const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren(
+ L1L2L3L4L5L6L7RouteChildren,
+)
+
+interface L1L2L3L4L5L6RouteChildren {
+ L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren
+}
+
+const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = {
+ L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren,
+}
+
+const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren(
+ L1L2L3L4L5L6RouteChildren,
+)
+
+interface L1L2L3L4L5RouteChildren {
+ L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren
+}
+
+const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = {
+ L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren,
+}
+
+const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren(
+ L1L2L3L4L5RouteChildren,
+)
+
+interface L1L2L3L4RouteChildren {
+ L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren
+}
+
+const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = {
+ L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren,
+}
+
+const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren(
+ L1L2L3L4RouteChildren,
+)
+
+interface L1L2L3RouteChildren {
+ L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren
+}
+
+const L1L2L3RouteChildren: L1L2L3RouteChildren = {
+ L1L2L3L4Route: L1L2L3L4RouteWithChildren,
+}
+
+const L1L2L3RouteWithChildren =
+ L1L2L3Route._addFileChildren(L1L2L3RouteChildren)
+
+interface L1L2RouteChildren {
+ L1L2L3Route: typeof L1L2L3RouteWithChildren
+}
+
+const L1L2RouteChildren: L1L2RouteChildren = {
+ L1L2L3Route: L1L2L3RouteWithChildren,
+}
+
+const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren)
+
+interface L1RouteChildren {
+ L1L2Route: typeof L1L2RouteWithChildren
+}
+
+const L1RouteChildren: L1RouteChildren = {
+ L1L2Route: L1L2RouteWithChildren,
+}
+
+const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ L1Route: L1RouteWithChildren,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx
new file mode 100644
index 0000000000..7c4eb0babe
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..c5f9de6922
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx
@@ -0,0 +1,27 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx
new file mode 100644
index 0000000000..25099720e4
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return peak-large-page-index
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
new file mode 100644
index 0000000000..036a2a1911
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({
+ loader: () => makeLargePageLevelData(8, 0x5eed_1008),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelEightComponent,
+})
+
+function LevelEightComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
new file mode 100644
index 0000000000..0aceecf51b
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({
+ loader: () => makeLargePageLevelData(7, 0x5eed_1007),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelSevenComponent,
+})
+
+function LevelSevenComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx
new file mode 100644
index 0000000000..1bc6588434
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({
+ loader: () => makeLargePageLevelData(6, 0x5eed_1006),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelSixComponent,
+})
+
+function LevelSixComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx
new file mode 100644
index 0000000000..bbece2972c
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5')({
+ loader: () => makeLargePageLevelData(5, 0x5eed_1005),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelFiveComponent,
+})
+
+function LevelFiveComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx
new file mode 100644
index 0000000000..62822e75ea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4')({
+ loader: () => makeLargePageLevelData(4, 0x5eed_1004),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelFourComponent,
+})
+
+function LevelFourComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx
new file mode 100644
index 0000000000..4a74984f35
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3')({
+ loader: () => makeLargePageLevelData(3, 0x5eed_1003),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelThreeComponent,
+})
+
+function LevelThreeComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx
new file mode 100644
index 0000000000..a283fe04cb
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2')({
+ loader: () => makeLargePageLevelData(2, 0x5eed_1002),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelTwoComponent,
+})
+
+function LevelTwoComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx
new file mode 100644
index 0000000000..469a129b37
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1')({
+ loader: () => makeLargePageLevelData(1, 0x5eed_1001),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelOneComponent,
+})
+
+function LevelOneComponent() {
+ const data = Route.useLoaderData()
+ const first = data.records[0]!
+
+ return (
+
+ {data.marker}
+ records: {data.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json
new file mode 100644
index 0000000000..4d78884cea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts
new file mode 100644
index 0000000000..7d4fa70196
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server peak-large-page (react)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
new file mode 100644
index 0000000000..ecfa5bb266
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
@@ -0,0 +1,83 @@
+import { bench, describe } from 'vitest'
+import {
+ memoryBenchOptions,
+ randomSegment,
+ runSequentialRequestLoop,
+} from '../../../bench-utils'
+import type { StartRequestHandler } from '../../../bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0xdecafbad
+const requestChurnIterations = 200
+const itemPageMarker = 'data-bench="request-churn-item"'
+const dehydrationMarker = '$_TSR'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+)) as {
+ default: StartRequestHandler
+}
+
+function buildItemRequest(random: () => number) {
+ const id = randomSegment(random)
+ const q = `q-${randomSegment(random)}`
+
+ return new Request(`http://localhost/items/${id}?q=${q}`, requestInit)
+}
+
+function validateItemResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateItemBody(body: string) {
+ if (!body.includes(itemPageMarker)) {
+ throw new Error('Expected request-churn item marker in response body')
+ }
+}
+
+async function assertRequestChurnSanity(handler: StartRequestHandler) {
+ const response = await handler.fetch(
+ new Request('http://localhost/items/sanity-item?q=q-sanity', requestInit),
+ )
+ const body = await response.text()
+
+ if (response.status !== 200) {
+ throw new Error(`Expected sanity status 200, got ${response.status}`)
+ }
+
+ validateItemBody(body)
+
+ if (!body.includes(dehydrationMarker)) {
+ throw new Error(
+ 'Expected sanity response to include the dehydration marker',
+ )
+ }
+}
+
+await assertRequestChurnSanity(handler)
+
+describe('memory', () => {
+ bench(
+ 'mem request-churn (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: requestChurnIterations,
+ buildRequest: buildItemRequest,
+ validateResponse: validateItemResponse,
+ validateBody: validateItemBody,
+ }),
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/project.json b/benchmarks/memory/server/scenarios/request-churn/react/project.json
new file mode 100644
index 0000000000..03dce4042a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-server-request-churn-react",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..d22f1bee18
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/items/$id'
+ id: '__root__' | '/' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx
new file mode 100644
index 0000000000..7c4eb0babe
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..c5f9de6922
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx
@@ -0,0 +1,27 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx
new file mode 100644
index 0000000000..e6335b0d83
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return request-churn-index
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx
new file mode 100644
index 0000000000..5fd86ee5fb
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx
@@ -0,0 +1,40 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+const itemIndexes = Array.from({ length: 5 }, (_, index) => index)
+
+type ItemSearch = {
+ q: string
+}
+
+export const Route = createFileRoute('/items/$id')({
+ validateSearch: (search: Record): ItemSearch => ({
+ q: typeof search.q === 'string' ? search.q : '',
+ }),
+ loaderDeps: ({ search }) => ({ q: search.q }),
+ loader: ({ params, deps }) => ({
+ id: params.id,
+ title: `Item ${params.id}`,
+ q: deps.q,
+ items: itemIndexes.map((index) => ({
+ id: `${params.id}-${index}`,
+ label: `${deps.q}-${index}`,
+ })),
+ }),
+ component: ItemComponent,
+})
+
+function ItemComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {data.title}
+ {data.q}
+
+ {data.items.map((item) => (
+ - {item.label}
+ ))}
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json
new file mode 100644
index 0000000000..4d78884cea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts b/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts
new file mode 100644
index 0000000000..4b75255eb1
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server request-churn (react)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
new file mode 100644
index 0000000000..50952fef6c
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
@@ -0,0 +1,105 @@
+import { bench, describe } from 'vitest'
+import {
+ memoryBenchOptions,
+ randomSegment,
+ runSequentialRequestLoop,
+} from '../../../bench-utils'
+import type { StartRequestHandler } from '../../../bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0x51eaa11
+const serializationPayloadIterations = 5
+const payloadPageMarker = 'data-bench="serialization-payload"'
+const dehydrationMarker = '$_TSR'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+)) as {
+ default: StartRequestHandler
+}
+
+function buildPayloadRequest(random: () => number, index: number) {
+ const id = `payload-${index}-${randomSegment(random)}`
+
+ return new Request(`http://localhost/data/${id}`, requestInit)
+}
+
+function knownMapKey(id: string) {
+ return `map-${id}-000`
+}
+
+function getRequestId(request: Request) {
+ const url = new URL(request.url)
+ const match = /^\/data\/([^/]+)$/.exec(url.pathname)
+ const id = match?.[1]
+
+ if (id === undefined) {
+ throw new Error(`Expected /data/$id request URL, got ${request.url}`)
+ }
+
+ return decodeURIComponent(id)
+}
+
+function validatePayloadResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validatePayloadBody(
+ body: string,
+ _response: Response,
+ request: Request,
+) {
+ if (!body.includes(payloadPageMarker)) {
+ throw new Error('Expected serialization-payload marker in response body')
+ }
+
+ if (!body.includes(dehydrationMarker)) {
+ throw new Error('Expected serialization-payload dehydration script in body')
+ }
+
+ const mapKey = knownMapKey(getRequestId(request))
+
+ if (!body.includes(mapKey)) {
+ throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`)
+ }
+}
+
+async function assertSerializationPayloadSanity(handler: StartRequestHandler) {
+ const request = new Request(
+ 'http://localhost/data/sanity-payload',
+ requestInit,
+ )
+ const response = await handler.fetch(request)
+ const body = await response.text()
+
+ validatePayloadResponse(response, request)
+ validatePayloadBody(body, response, request)
+}
+
+await assertSerializationPayloadSanity(handler)
+
+describe('memory', () => {
+ bench(
+ 'mem serialization-payload (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: serializationPayloadIterations,
+ buildRequest: buildPayloadRequest,
+ validateResponse: validatePayloadResponse,
+ validateBody: validatePayloadBody,
+ }),
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/project.json b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json
new file mode 100644
index 0000000000..5cc35a789f
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-server-serialization-payload-react",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..7e370fd84c
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts
@@ -0,0 +1,68 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as DataIdRouteImport } from './routes/data.$id'
+
+const DataIdRoute = DataIdRouteImport.update({
+ id: '/data/$id',
+ path: '/data/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/data/$id': typeof DataIdRoute
+}
+export interface FileRoutesByTo {
+ '/data/$id': typeof DataIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/data/$id': typeof DataIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/data/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/data/$id'
+ id: '__root__' | '/data/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ DataIdRoute: typeof DataIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/data/$id': {
+ id: '/data/$id'
+ path: '/data/$id'
+ fullPath: '/data/$id'
+ preLoaderRoute: typeof DataIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ DataIdRoute: DataIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx
new file mode 100644
index 0000000000..7c4eb0babe
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..c5f9de6922
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx
@@ -0,0 +1,27 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx
new file mode 100644
index 0000000000..a131991bc6
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx
@@ -0,0 +1,130 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+const mapEntryCount = 500
+const setEntryCount = 500
+const temporalEntryCount = 500
+const nestedTreeDepth = 5
+const nestedTreeBreadth = 6
+const payloadTextLength = 150
+
+interface MapPayloadValue {
+ index: number
+ label: string
+ createdAt: Date
+ count: bigint
+ text: string
+}
+
+interface NestedPayloadNode {
+ id: string
+ depth: number
+ text: string
+ values: Array
+ children: Array
+}
+
+export interface SerializationPayload {
+ id: string
+ lookup: Map
+ tags: Set
+ dates: Array
+ bigints: Array
+ tree: NestedPayloadNode
+}
+
+export const Route = createFileRoute('/data/$id')({
+ loader: ({ params }) => makeSerializationPayload(params.id),
+ component: DataComponent,
+})
+
+function DataComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+ Map size: {data.lookup.size}
+ )
+}
+
+function makeSerializationPayload(id: string): SerializationPayload {
+ const hash = hashId(id)
+ const baseTimestamp = Date.UTC(2024, 0, 1) + hash
+
+ return {
+ id,
+ lookup: new Map(
+ Array.from(
+ { length: mapEntryCount },
+ (_, index): [string, MapPayloadValue] => [
+ makeMapKey(id, index),
+ {
+ index,
+ label: `${id}-map-${index}`,
+ createdAt: new Date(baseTimestamp + index * 1_000),
+ count: BigInt(hash) * 10_000n + BigInt(index),
+ text: makePayloadText(id, `map-${index}`),
+ },
+ ],
+ ),
+ ),
+ tags: new Set(
+ Array.from({ length: setEntryCount }, (_, index) =>
+ makePayloadText(id, `set-${index}`),
+ ),
+ ),
+ dates: Array.from(
+ { length: temporalEntryCount },
+ (_, index) => new Date(baseTimestamp + index * 60_000),
+ ),
+ bigints: Array.from(
+ { length: temporalEntryCount },
+ (_, index) => BigInt(hash) * 1_000_000n + BigInt(index),
+ ),
+ tree: makeNestedTree(id, 0, 'root', hash),
+ }
+}
+
+function makeMapKey(id: string, index: number) {
+ return `map-${id}-${index.toString().padStart(3, '0')}`
+}
+
+function makeNestedTree(
+ id: string,
+ depth: number,
+ path: string,
+ hash: number,
+): NestedPayloadNode {
+ return {
+ id: `${id}-node-${path}`,
+ depth,
+ text: makePayloadText(id, `tree-${depth}-${path}`),
+ values: [hash, depth, path.length],
+ children:
+ depth === nestedTreeDepth
+ ? []
+ : Array.from({ length: nestedTreeBreadth }, (_, index) =>
+ makeNestedTree(id, depth + 1, `${path}-${index}`, hash),
+ ),
+ }
+}
+
+function makePayloadText(id: string, segment: string) {
+ const filler = 'abcdefghijklmnopqrstuvwxyz0123456789'
+ let value = `${id}|${segment}|`
+
+ while (value.length < payloadTextLength) {
+ value += filler
+ }
+
+ return value.slice(0, payloadTextLength)
+}
+
+function hashId(id: string) {
+ let hash = 2_166_136_261
+
+ for (let index = 0; index < id.length; index++) {
+ hash ^= id.charCodeAt(index)
+ hash = Math.imul(hash, 16_777_619)
+ }
+
+ return hash >>> 0
+}
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json
new file mode 100644
index 0000000000..4d78884cea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts
new file mode 100644
index 0000000000..cfcc305c1a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server serialization-payload (react)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
new file mode 100644
index 0000000000..1bb5899103
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
@@ -0,0 +1,261 @@
+import { bench, describe } from 'vitest'
+import {
+ createDeterministicRandom,
+ memoryBenchOptions,
+ randomSegment,
+ runSequentialRequestLoop,
+} from '../../../bench-utils'
+import type { StartRequestHandler } from '../../../bench-utils'
+
+type FnUrls = {
+ get: string
+ post: string
+}
+
+type PayloadFixture = {
+ id: string
+ body: string
+ query: string
+}
+
+type SerovalNode =
+ | {
+ t: 1
+ s: string
+ }
+ | {
+ t: 10
+ i: number
+ p: {
+ k: Array
+ v: Array
+ }
+ o: number
+ }
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0xdecafbad
+const payloadSeed = 0x51f0cafe
+const fixtureCount = 16
+const serverFnChurnIterations = 150
+const origin = 'http://localhost'
+const tssContentTypeFramed = 'application/x-tss-framed'
+const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json`
+const xTssSerialized = 'x-tss-serialized'
+const contextMarker = 'ctx-server-fn-churn'
+const expectedIdsByRequest = new WeakMap()
+
+const commonHeaders = {
+ 'x-tsr-serverFn': 'true',
+ 'sec-fetch-site': 'same-origin',
+ accept: acceptHeader,
+} satisfies HeadersInit
+
+const postHeaders = {
+ ...commonHeaders,
+ 'content-type': 'application/json',
+} satisfies HeadersInit
+
+// Hand-rolled copy of Start's seroval RPC wire format so POST bodies can be
+// precomputed at module level. Coupled to the internal protocol on purpose;
+// the module-load sanity check below throws loudly if the protocol drifts.
+function stringNode(value: string): SerovalNode {
+ return { t: 1, s: value }
+}
+
+function objectNode(
+ id: number,
+ entries: Array,
+): SerovalNode {
+ return {
+ t: 10,
+ i: id,
+ p: {
+ k: entries.map(([key]) => key),
+ v: entries.map(([, value]) => value),
+ },
+ o: 0,
+ }
+}
+
+function serializePayload(id: string) {
+ return JSON.stringify({
+ t: objectNode(0, [['data', objectNode(1, [['id', stringNode(id)]])]]),
+ f: 63,
+ m: [],
+ })
+}
+
+function createFixtures(kind: 'get' | 'post') {
+ const random = createDeterministicRandom(payloadSeed ^ kind.length)
+
+ return Array.from({ length: fixtureCount }, (_, index): PayloadFixture => {
+ const id = [kind, index, randomSegment(random), randomSegment(random)].join(
+ '-',
+ )
+ const body = serializePayload(id)
+
+ return {
+ id,
+ body,
+ query: `?${new URLSearchParams({ payload: body })}`,
+ }
+ })
+}
+
+const getFixtures = createFixtures('get')
+const postFixtures = createFixtures('post')
+
+const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+)) as {
+ default: StartRequestHandler
+}
+
+async function discoverUrls(handler: StartRequestHandler) {
+ const response = await handler.fetch(new Request(`${origin}/api/fn-urls`))
+ const text = await response.text()
+
+ if (response.status !== 200) {
+ throw new Error(
+ `URL discovery failed with status ${response.status}: ${text}`,
+ )
+ }
+
+ let urls: Partial
+
+ try {
+ urls = JSON.parse(text) as Partial
+ } catch (error) {
+ throw new Error(`URL discovery returned invalid JSON: ${text}`, {
+ cause: error,
+ })
+ }
+
+ if (typeof urls.get !== 'string' || typeof urls.post !== 'string') {
+ throw new Error(`URL discovery returned invalid payload: ${text}`)
+ }
+
+ return urls as FnUrls
+}
+
+function buildGetRequest(url: string, fixture: PayloadFixture) {
+ return new Request(`${origin}${url}${fixture.query}`, {
+ method: 'GET',
+ headers: commonHeaders,
+ })
+}
+
+function buildPostRequest(url: string, fixture: PayloadFixture) {
+ return new Request(`${origin}${url}`, {
+ method: 'POST',
+ headers: postHeaders,
+ body: fixture.body,
+ })
+}
+
+function validateServerFnResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+
+ if (!response.headers.get(xTssSerialized)) {
+ throw new Error(`Expected ${xTssSerialized} header for ${request.url}`)
+ }
+}
+
+function validateEchoedBody(
+ body: string,
+ request: Request,
+ expectedId: string,
+) {
+ if (!body.includes(expectedId)) {
+ throw new Error(`Expected echoed id ${expectedId} in ${request.url}`)
+ }
+
+ if (!body.includes(contextMarker)) {
+ throw new Error(
+ `Expected context marker ${contextMarker} in ${request.url}`,
+ )
+ }
+
+ if (!body.includes(`${expectedId}-4`)) {
+ throw new Error(`Expected final payload record for ${expectedId}`)
+ }
+}
+
+function validateServerFnBody(
+ body: string,
+ _response: Response,
+ request: Request,
+) {
+ const expectedId = expectedIdsByRequest.get(request)
+
+ if (expectedId) {
+ validateEchoedBody(body, request, expectedId)
+ }
+}
+
+async function assertServerFnChurnSanity(
+ handler: StartRequestHandler,
+ urls: FnUrls,
+) {
+ const getFixture = getFixtures[0]!
+ const getRequest = buildGetRequest(urls.get, getFixture)
+ const getResponse = await handler.fetch(getRequest)
+ const getBody = await getResponse.text()
+
+ validateServerFnResponse(getResponse, getRequest)
+ validateEchoedBody(getBody, getRequest, getFixture.id)
+
+ const postFixture = postFixtures[0]!
+ const postRequest = buildPostRequest(urls.post, postFixture)
+ const postResponse = await handler.fetch(postRequest)
+ const postBody = await postResponse.text()
+
+ validateServerFnResponse(postResponse, postRequest)
+ validateEchoedBody(postBody, postRequest, postFixture.id)
+}
+
+const urls = await discoverUrls(handler)
+
+await assertServerFnChurnSanity(handler, urls)
+
+describe('memory', () => {
+ bench(
+ 'mem server-fn-churn (react)',
+ () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: serverFnChurnIterations,
+ buildRequest: (_random, index) => {
+ const fixtureIndex = Math.floor(index / 2) % fixtureCount
+
+ if (index % 2 === 0) {
+ const fixture = getFixtures[fixtureIndex]!
+ const request = buildGetRequest(urls.get, fixture)
+
+ if (index === 0) {
+ expectedIdsByRequest.set(request, fixture.id)
+ }
+
+ return request
+ }
+
+ const fixture = postFixtures[fixtureIndex]!
+ const request = buildPostRequest(urls.post, fixture)
+
+ if (index === 1) {
+ expectedIdsByRequest.set(request, fixture.id)
+ }
+
+ return request
+ },
+ validateResponse: validateServerFnResponse,
+ validateBody: validateServerFnBody,
+ }),
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json
new file mode 100644
index 0000000000..926a50b9f1
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-server-server-fn-churn-react",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts
new file mode 100644
index 0000000000..2ea8643da1
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts
@@ -0,0 +1,47 @@
+import { createMiddleware, createServerFn } from '@tanstack/react-start'
+
+type ServerFnInput = {
+ id: string
+}
+
+const recordIndexes = Array.from({ length: 5 }, (_, index) => index)
+
+const contextMiddleware = createMiddleware({ type: 'function' }).server(
+ ({ next }) =>
+ next({
+ context: {
+ ctx: 'ctx-server-fn-churn',
+ },
+ }),
+)
+
+function validateInput(input: unknown): ServerFnInput {
+ const payload = input as Partial | null
+
+ if (typeof payload?.id !== 'string') {
+ throw new Error('invalid server-fn churn input')
+ }
+
+ return { id: payload.id }
+}
+
+function echoPayload(data: ServerFnInput, context: { ctx: string }) {
+ return {
+ id: data.id,
+ ctx: context.ctx,
+ payload: recordIndexes.map((index) => ({
+ id: `${data.id}-${index}`,
+ label: `record-${index}`,
+ })),
+ }
+}
+
+export const churnGet = createServerFn({ method: 'GET' })
+ .middleware([contextMiddleware])
+ .validator(validateInput)
+ .handler(({ data, context }) => echoPayload(data, context))
+
+export const churnPost = createServerFn({ method: 'POST' })
+ .middleware([contextMiddleware])
+ .validator(validateInput)
+ .handler(({ data, context }) => echoPayload(data, context))
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..10e933da3d
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({
+ id: '/api/fn-urls',
+ path: '/api/fn-urls',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/api/fn-urls': typeof ApiFnUrlsRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/api/fn-urls': typeof ApiFnUrlsRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/api/fn-urls': typeof ApiFnUrlsRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/api/fn-urls'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/api/fn-urls'
+ id: '__root__' | '/' | '/api/fn-urls'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ ApiFnUrlsRoute: typeof ApiFnUrlsRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/api/fn-urls': {
+ id: '/api/fn-urls'
+ path: '/api/fn-urls'
+ fullPath: '/api/fn-urls'
+ preLoaderRoute: typeof ApiFnUrlsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ ApiFnUrlsRoute: ApiFnUrlsRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx
new file mode 100644
index 0000000000..7c4eb0babe
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..c5f9de6922
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx
@@ -0,0 +1,27 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts
new file mode 100644
index 0000000000..da74a20f62
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { churnGet, churnPost } from '../fns'
+
+export const Route = createFileRoute('/api/fn-urls')({
+ server: {
+ handlers: {
+ GET: () =>
+ Response.json({
+ get: churnGet.url,
+ post: churnPost.url,
+ }),
+ },
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx
new file mode 100644
index 0000000000..de0acfc2b3
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { churnGet, churnPost } from '../fns'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+ memory-server-fn-churn-index
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json
new file mode 100644
index 0000000000..4d78884cea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts
new file mode 100644
index 0000000000..8a3eea0d36
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server server-fn-churn (react)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
new file mode 100644
index 0000000000..8b706c48b0
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
@@ -0,0 +1,217 @@
+import { bench, describe } from 'vitest'
+import {
+ createDeterministicRandom,
+ memoryBenchOptions,
+ randomSegment,
+} from '../../../bench-utils'
+import type { StartRequestHandler } from '../../../bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0xdecafbad
+const streamingPeakIterations = 3
+const fallbackMarkers = [
+ 'streaming-peak-fallback-0',
+ 'streaming-peak-fallback-1',
+ 'streaming-peak-fallback-2',
+ 'streaming-peak-fallback-3',
+] as const
+const deferredSectionMarkers = [
+ 'streaming-peak-deferred-0',
+ 'streaming-peak-deferred-1',
+ 'streaming-peak-deferred-2',
+ 'streaming-peak-deferred-3',
+] as const
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+)) as {
+ default: StartRequestHandler
+}
+
+function buildStreamingRequest(random: () => number, index: number) {
+ return new Request(
+ `http://localhost/stream/${index}-${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function validateStreamingResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function getResponseReader(response: Response) {
+ const reader = response.body?.getReader()
+
+ if (!reader) {
+ throw new Error('Expected streaming response body')
+ }
+
+ return reader
+}
+
+async function consumeResponseChunks(response: Response) {
+ const reader = getResponseReader(response)
+ let chunkCount = 0
+
+ while (true) {
+ const result = await reader.read()
+
+ if (result.done) {
+ break
+ }
+
+ chunkCount++
+ }
+
+ return chunkCount
+}
+
+async function readStreamingBody(response: Response) {
+ const reader = getResponseReader(response)
+ const decoder = new TextDecoder()
+ let body = ''
+ let chunkCount = 0
+
+ while (true) {
+ const result = await reader.read()
+
+ if (result.done) {
+ break
+ }
+
+ chunkCount++
+ body += decoder.decode(result.value, { stream: true })
+ }
+
+ body += decoder.decode()
+
+ return { body, chunkCount }
+}
+
+function assertFallbacksPrecedeDeferredContent(body: string) {
+ for (let index = 0; index < fallbackMarkers.length; index++) {
+ const fallbackIndex = body.indexOf(fallbackMarkers[index]!)
+ const deferredIndex = body.indexOf(deferredSectionMarkers[index]!)
+
+ if (fallbackIndex === -1) {
+ throw new Error(
+ `Expected fallback marker ${fallbackMarkers[index]} in body`,
+ )
+ }
+
+ if (deferredIndex === -1) {
+ throw new Error(
+ `Expected deferred section marker ${deferredSectionMarkers[index]} in body`,
+ )
+ }
+
+ if (fallbackIndex > deferredIndex) {
+ throw new Error(
+ `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`,
+ )
+ }
+ }
+}
+
+function assertBufferedBodyContainsDeferredSections(body: string) {
+ for (const marker of deferredSectionMarkers) {
+ if (!body.includes(marker)) {
+ throw new Error(`Expected buffered body to contain ${marker}`)
+ }
+ }
+}
+
+async function assertStreamingPeakSanity(handler: StartRequestHandler) {
+ const chunkedRequest = new Request(
+ 'http://localhost/stream/sanity-chunked',
+ requestInit,
+ )
+ const chunkedResponse = await handler.fetch(chunkedRequest)
+
+ validateStreamingResponse(chunkedResponse, chunkedRequest)
+
+ const chunked = await readStreamingBody(chunkedResponse)
+
+ if (chunked.chunkCount <= 1) {
+ throw new Error(
+ `Expected chunked sanity response to produce multiple chunks, got ${chunked.chunkCount}`,
+ )
+ }
+
+ assertFallbacksPrecedeDeferredContent(chunked.body)
+
+ const bufferedRequest = new Request(
+ 'http://localhost/stream/sanity-buffered',
+ requestInit,
+ )
+ const bufferedResponse = await handler.fetch(bufferedRequest)
+
+ validateStreamingResponse(bufferedResponse, bufferedRequest)
+
+ const bufferedBody = await bufferedResponse.text()
+
+ assertBufferedBodyContainsDeferredSections(bufferedBody)
+}
+
+async function runChunkedStreamingLoop(handler: StartRequestHandler) {
+ const random = createDeterministicRandom(benchmarkSeed)
+ let totalChunkCount = 0
+
+ for (let index = 0; index < streamingPeakIterations; index++) {
+ const request = buildStreamingRequest(random, index)
+ const response = await handler.fetch(request)
+
+ validateStreamingResponse(response, request)
+ totalChunkCount += await consumeResponseChunks(response)
+ }
+
+ return totalChunkCount
+}
+
+async function runBufferedStreamingLoop(handler: StartRequestHandler) {
+ const random = createDeterministicRandom(benchmarkSeed)
+ let totalBodyLength = 0
+
+ for (let index = 0; index < streamingPeakIterations; index++) {
+ const request = buildStreamingRequest(random, index)
+ const response = await handler.fetch(request)
+
+ validateStreamingResponse(response, request)
+
+ const body = await response.text()
+ totalBodyLength += body.length
+ }
+
+ return totalBodyLength
+}
+
+await assertStreamingPeakSanity(handler)
+
+describe('memory', () => {
+ bench(
+ 'mem streaming-peak chunked (react)',
+ async () => {
+ await runChunkedStreamingLoop(handler)
+ },
+ memoryBenchOptions,
+ )
+
+ bench(
+ 'mem streaming-peak buffered (react)',
+ async () => {
+ await runBufferedStreamingLoop(handler)
+ },
+ memoryBenchOptions,
+ )
+})
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/project.json b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json
new file mode 100644
index 0000000000..d6fb362055
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json
@@ -0,0 +1,31 @@
+{
+ "name": "@benchmarks/memory-server-streaming-peak-react",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts
new file mode 100644
index 0000000000..fbbc25ecfb
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as StreamIdRouteImport } from './routes/stream.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const StreamIdRoute = StreamIdRouteImport.update({
+ id: '/stream/$id',
+ path: '/stream/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/stream/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/stream/$id'
+ id: '__root__' | '/' | '/stream/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StreamIdRoute: typeof StreamIdRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/stream/$id': {
+ id: '/stream/$id'
+ path: '/stream/$id'
+ fullPath: '/stream/$id'
+ preLoaderRoute: typeof StreamIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StreamIdRoute: StreamIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx
new file mode 100644
index 0000000000..7c4eb0babe
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx
new file mode 100644
index 0000000000..c5f9de6922
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx
@@ -0,0 +1,27 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx
new file mode 100644
index 0000000000..669fdef2e1
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return streaming-peak-index
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx
new file mode 100644
index 0000000000..dc93600b47
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx
@@ -0,0 +1,107 @@
+import { Await, createFileRoute } from '@tanstack/react-router'
+import { Suspense } from 'react'
+
+const deferredRecordCount = 250
+const recordValueLength = 128
+const fallbackFlushDelayMs = 1
+
+interface DeferredRecord {
+ id: string
+ value: string
+}
+
+export interface DeferredSectionPayload {
+ index: number
+ records: Array
+}
+
+export const Route = createFileRoute('/stream/$id')({
+ loader: ({ params }) => ({
+ eager: `streaming-peak-eager-${params.id}`,
+ deferred0: makeDeferredSection(params.id, 0),
+ deferred1: makeDeferredSection(params.id, 1),
+ deferred2: makeDeferredSection(params.id, 2),
+ deferred3: makeDeferredSection(params.id, 3),
+ }),
+ component: StreamComponent,
+})
+
+// Deferred sections must settle strictly AFTER React's shell flush, which
+// React schedules via setImmediate internally. Microtask chains drain during
+// router load (sections resolve before the Suspense boundaries are even
+// reached) and setImmediate chains registered at loader time win the race
+// against React's flush — either way no fallback ever streams and the bench
+// stops exercising multi-flush streaming. Timer-phase callbacks reliably lose
+// that race, so this is the documented exception to the no-timers convention:
+// the few ms of wall-clock are irrelevant to memory metrics, and distinct
+// delays keep section ordering deterministic.
+function afterFallbackFlush(sectionIndex: number) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, fallbackFlushDelayMs + sectionIndex)
+ })
+}
+
+function makeRecordValue(
+ id: string,
+ sectionIndex: number,
+ recordIndex: number,
+) {
+ const token = `${id}:${sectionIndex}:${recordIndex}:streaming-peak-record;`
+
+ return token
+ .repeat(Math.ceil(recordValueLength / token.length))
+ .slice(0, recordValueLength)
+}
+
+function makeDeferredSection(id: string, sectionIndex: number) {
+ return afterFallbackFlush(sectionIndex).then(() => ({
+ index: sectionIndex,
+ records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({
+ id: `${id}-${sectionIndex}-${recordIndex}`,
+ value: makeRecordValue(id, sectionIndex, recordIndex),
+ })),
+ }))
+}
+
+function StreamComponent() {
+ const data = Route.useLoaderData()
+ const deferredSections = [
+ { index: 0, promise: data.deferred0 },
+ { index: 1, promise: data.deferred1 },
+ { index: 2, promise: data.deferred2 },
+ { index: 3, promise: data.deferred3 },
+ ] as const
+
+ return (
+
+ {data.eager}
+ {deferredSections.map(({ index, promise }) => (
+
+ streaming-peak-fallback-{index}
+
+ }
+ >
+
+ {(section) => }
+
+
+ ))}
+
+ )
+}
+
+function DeferredSection({ section }: { section: DeferredSectionPayload }) {
+ const marker = `streaming-peak-deferred-${section.index}`
+
+ return (
+
+ {marker}
+ {section.records.map((record) => (
+ {record.value}
+ ))}
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json
new file mode 100644
index 0000000000..4d78884cea
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts
new file mode 100644
index 0000000000..733aabb76d
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import react from '@vitejs/plugin-react'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ react(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server streaming-peak (react)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/tsconfig.json b/benchmarks/memory/server/tsconfig.json
new file mode 100644
index 0000000000..5d5dcbf825
--- /dev/null
+++ b/benchmarks/memory/server/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": ["bench-utils.ts"]
+}
diff --git a/benchmarks/memory/server/vitest.react.config.ts b/benchmarks/memory/server/vitest.react.config.ts
new file mode 100644
index 0000000000..356ab7b3a8
--- /dev/null
+++ b/benchmarks/memory/server/vitest.react.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ watch: false,
+ projects: ['./scenarios/*/react/vite.config.ts'],
+ },
+})
diff --git a/packages/react-router/src/ssr/renderRouterToStream.tsx b/packages/react-router/src/ssr/renderRouterToStream.tsx
index d42a22396a..a5614b5e50 100644
--- a/packages/react-router/src/ssr/renderRouterToStream.tsx
+++ b/packages/react-router/src/ssr/renderRouterToStream.tsx
@@ -46,11 +46,21 @@ export const renderRouterToStream = async ({
responseHeaders: Headers
children: ReactNode
}) => {
+ // A client disconnecting mid-stream is normal operation, not a render
+ // failure; don't let React's onError log it as one.
+ const isAbortError = (error: unknown) =>
+ (request.signal.aborted && error === request.signal.reason) ||
+ (error instanceof Error && error.name === 'AbortError')
+
if (typeof ReactDOMServer.renderToReadableStream === 'function') {
const stream = await ReactDOMServer.renderToReadableStream(children, {
signal: request.signal,
nonce: router.options.ssr?.nonce,
progressiveChunkSize: Number.POSITIVE_INFINITY,
+ onError: (error, info) => {
+ if (isAbortError(error)) return
+ console.error('Error in renderToReadableStream:', error, info)
+ },
})
if (isbot(request.headers.get('User-Agent'))) {
@@ -151,7 +161,9 @@ export const renderRouterToStream = async ({
},
}),
onError: (error, info) => {
- console.error('Error in renderToPipeableStream:', error, info)
+ if (!isAbortError(error)) {
+ console.error('Error in renderToPipeableStream:', error, info)
+ }
abortPipeable(error, { defaultError: true })
},
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index efb2ac9b5f..b30d7b42a1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -313,6 +313,74 @@ importers:
specifier: ^4.1.4
version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ benchmarks/memory/client:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/router-core':
+ specifier: workspace:*
+ version: link:../../../packages/router-core
+ react:
+ specifier: ^19.2.3
+ version: 19.2.3
+ react-dom:
+ specifier: ^19.2.3
+ version: 19.2.3(react@19.2.3)
+ devDependencies:
+ '@codspeed/vitest-plugin':
+ specifier: ^5.5.0
+ version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)
+ '@testing-library/react':
+ specifier: ^16.2.0
+ version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@types/jsdom':
+ specifier: 28.0.0
+ version: 28.0.0
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ typescript:
+ specifier: ^6.0.2
+ version: 6.0.2
+ vite:
+ specifier: ^8.0.14
+ version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)
+ vitest:
+ specifier: ^4.1.4
+ version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+
+ benchmarks/memory/server:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/react-start':
+ specifier: workspace:*
+ version: link:../../../packages/react-start
+ react:
+ specifier: ^19.2.3
+ version: 19.2.3
+ react-dom:
+ specifier: ^19.2.3
+ version: 19.2.3(react@19.2.3)
+ devDependencies:
+ '@codspeed/vitest-plugin':
+ specifier: ^5.5.0
+ version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ typescript:
+ specifier: ^6.0.2
+ version: 6.0.2
+ vite:
+ specifier: ^8.0.14
+ version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)
+ vitest:
+ specifier: ^4.1.4
+ version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+
benchmarks/ssr:
dependencies:
'@tanstack/react-router':
@@ -2024,7 +2092,7 @@ importers:
version: 2.0.1
'@rsbuild/plugin-react':
specifier: ^2.0.0
- version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23))
+ version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21))
'@tanstack/router-e2e-utils':
specifier: workspace:^
version: link:../../e2e-utils
@@ -31449,9 +31517,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23))':
+ '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21))':
dependencies:
- '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0)
+ '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0)
react-refresh: 0.18.0
optionalDependencies:
'@rsbuild/core': 2.0.1
@@ -31642,6 +31710,13 @@ snapshots:
optionalDependencies:
'@swc/helpers': 0.5.23
+ '@rspack/core@2.0.5(@swc/helpers@0.5.21)':
+ dependencies:
+ '@rspack/binding': 2.0.5
+ optionalDependencies:
+ '@swc/helpers': 0.5.21
+ optional: true
+
'@rspack/core@2.0.5(@swc/helpers@0.5.23)':
dependencies:
'@rspack/binding': 2.0.5
@@ -31650,6 +31725,12 @@ snapshots:
'@rspack/lite-tapable@1.1.0': {}
+ '@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0)':
+ dependencies:
+ react-refresh: 0.18.0
+ optionalDependencies:
+ '@rspack/core': 2.0.5(@swc/helpers@0.5.21)
+
'@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0)':
dependencies:
react-refresh: 0.18.0
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 25cba27b56..2c130509f2 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -7,6 +7,7 @@ trustPolicy: 'no-downgrade'
packages:
- 'packages/*'
- 'benchmarks/*'
+ - 'benchmarks/memory/*'
- 'examples/react/*'
- 'examples/solid/*'
- 'examples/vue/*'
From 3d31b79315a27fd0ebe2c62999290cd7ab56e752 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 00:37:45 +0200
Subject: [PATCH 02/24] fix codspeed github action
---
.github/workflows/client-nav-benchmarks.yml | 31 +++++++++++++
.github/workflows/memory-benchmarks.yml | 49 ---------------------
2 files changed, 31 insertions(+), 49 deletions(-)
delete mode 100644 .github/workflows/memory-benchmarks.yml
diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml
index cd47ca3ba1..0e9de2eb4b 100644
--- a/.github/workflows/client-nav-benchmarks.yml
+++ b/.github/workflows/client-nav-benchmarks.yml
@@ -51,3 +51,34 @@ jobs:
with:
mode: simulation
run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }}
+
+ # TODO: once we have solid and vue memory benchmarks, we can merge the 2 jobs into one, and
+ # simply add `memory-server` and `memory-client` to the `benchmark` matrix above. For now, we only run memory benchmarks for react.
+ # PS: that unification is only possible if we can link "benchmark name" with "mode: simulation|memory"
+ memory-benchmarks:
+ name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark
+ strategy:
+ fail-fast: false
+ matrix:
+ benchmark:
+ - memory-server
+ - memory-client
+ framework:
+ - react
+ # - solid
+ # - vue
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Tools
+ uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main
+
+ - name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark
+ uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
+ with:
+ mode: memory
+ run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }}
diff --git a/.github/workflows/memory-benchmarks.yml b/.github/workflows/memory-benchmarks.yml
deleted file mode 100644
index a7c23f8a1b..0000000000
--- a/.github/workflows/memory-benchmarks.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-# Setup taken from https://codspeed.io/docs/benchmarks/nodejs/vitest
-name: Memory Benchmarks
-
-on:
- push:
- branches:
- - 'main'
- paths:
- - 'packages/**'
- - 'benchmarks/**'
- pull_request:
- paths:
- - 'packages/**'
- - 'benchmarks/**'
- workflow_dispatch:
-
-permissions:
- contents: read # required for actions/checkout
- id-token: write # required for OIDC authentication with CodSpeed
-
-env:
- NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- SERVER_PRESET: 'node-server'
- NX_NO_CLOUD: true
-
-jobs:
- benchmarks:
- name: Run ${{ matrix.benchmark }} CodSpeed benchmark
- strategy:
- fail-fast: false
- matrix:
- benchmark:
- - memory-server
- - memory-client
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
-
- - name: Setup Tools
- uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main
-
- - name: Run ${{ matrix.benchmark }} CodSpeed benchmark
- uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
- with:
- mode: memory
- run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:react
From 4c6d3aff8edf1a6a04c6ec0f771730fb938c03f4 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 00:44:56 +0200
Subject: [PATCH 03/24] unified codspeed job
---
.github/workflows/client-nav-benchmarks.yml | 52 ++++++++-------------
1 file changed, 20 insertions(+), 32 deletions(-)
diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml
index 0e9de2eb4b..e38a59ed6b 100644
--- a/.github/workflows/client-nav-benchmarks.yml
+++ b/.github/workflows/client-nav-benchmarks.yml
@@ -36,6 +36,25 @@ jobs:
benchmark:
- client-nav
- ssr
+ include:
+ - benchmark: client-nav
+ mode: simulation
+ - benchmark: ssr
+ mode: simulation
+ - benchmark: memory-server
+ mode: memory
+ - benchmark: memory-client
+ mode: memory
+ # TODO: temp exclude memory benchmarks for solid and vue until we have them working
+ exclude:
+ - benchmark: memory-server
+ framework: solid
+ - benchmark: memory-server
+ framework: vue
+ - benchmark: memory-client
+ framework: solid
+ - benchmark: memory-client
+ framework: vue
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -49,36 +68,5 @@ jobs:
- name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
with:
- mode: simulation
- run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }}
-
- # TODO: once we have solid and vue memory benchmarks, we can merge the 2 jobs into one, and
- # simply add `memory-server` and `memory-client` to the `benchmark` matrix above. For now, we only run memory benchmarks for react.
- # PS: that unification is only possible if we can link "benchmark name" with "mode: simulation|memory"
- memory-benchmarks:
- name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark
- strategy:
- fail-fast: false
- matrix:
- benchmark:
- - memory-server
- - memory-client
- framework:
- - react
- # - solid
- # - vue
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
-
- - name: Setup Tools
- uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main
-
- - name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark
- uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
- with:
- mode: memory
+ mode: ${{ matrix.mode }}
run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }}
From 025d4b40090fe1475931cfaf3fa3d432df3bf86d Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 00:52:33 +0200
Subject: [PATCH 04/24] im dumb
---
.github/workflows/client-nav-benchmarks.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml
index e38a59ed6b..553902f2ba 100644
--- a/.github/workflows/client-nav-benchmarks.yml
+++ b/.github/workflows/client-nav-benchmarks.yml
@@ -36,6 +36,8 @@ jobs:
benchmark:
- client-nav
- ssr
+ - memory-server
+ - memory-client
include:
- benchmark: client-nav
mode: simulation
From 537776c1da69b7019ff80970d525582d84a75ced Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 09:30:41 +0200
Subject: [PATCH 05/24] cleaner run command in github action
---
.github/workflows/client-nav-benchmarks.yml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml
index 553902f2ba..5db0e1af9c 100644
--- a/.github/workflows/client-nav-benchmarks.yml
+++ b/.github/workflows/client-nav-benchmarks.yml
@@ -71,4 +71,7 @@ jobs:
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
with:
mode: ${{ matrix.mode }}
- run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }}
+ run: >-
+ WITH_INSTRUMENTATION=1
+ pnpm nx run
+ @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }}
From 97d1801af862fd9247b5c8f6c5d281e4b6d28767 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 10:28:05 +0200
Subject: [PATCH 06/24] better response stream draining
---
benchmarks/memory/server/bench-utils.ts | 25 ++++++++++++++---
.../error-paths/react/memory.bench.ts | 2 --
.../peak-large-page/react/memory.bench.ts | 1 -
.../request-churn/react/memory.bench.ts | 1 -
.../react/memory.bench.ts | 1 -
.../server-fn-churn/react/memory.bench.ts | 28 ++-----------------
6 files changed, 23 insertions(+), 35 deletions(-)
diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts
index 2613d81828..4614152cc2 100644
--- a/benchmarks/memory/server/bench-utils.ts
+++ b/benchmarks/memory/server/bench-utils.ts
@@ -7,7 +7,6 @@ export interface RunSequentialRequestLoopOptions {
iterations?: number
buildRequest: (random: () => number, index: number) => Request
validateResponse?: (response: Response, request: Request) => void
- validateBody?: (body: string, response: Response, request: Request) => void
}
export const memoryBenchOptions = {
@@ -30,6 +29,26 @@ export function randomSegment(random: () => number) {
return Math.floor(random() * 1_000_000_000).toString(36)
}
+export async function drainResponse(response: Response) {
+ const reader = response.body?.getReader()
+
+ if (!reader) {
+ return
+ }
+
+ try {
+ while (true) {
+ const result = await reader.read()
+
+ if (result.done) {
+ break
+ }
+ }
+ } finally {
+ reader.releaseLock()
+ }
+}
+
export async function runSequentialRequestLoop(
handler: StartRequestHandler,
{
@@ -37,7 +56,6 @@ export async function runSequentialRequestLoop(
iterations = 10,
buildRequest,
validateResponse,
- validateBody,
}: RunSequentialRequestLoopOptions,
) {
const random = createDeterministicRandom(seed)
@@ -57,7 +75,6 @@ export async function runSequentialRequestLoop(
validate(response, request)
- const body = await response.text()
- validateBody?.(body, response, request)
+ await drainResponse(response)
}
}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
index 32b536881b..bbbfd63456 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
@@ -169,7 +169,6 @@ describe('memory', () => {
iterations: errorPathsIterations,
buildRequest: buildNotFoundRequest,
validateResponse: validateNotFoundResponse,
- validateBody: validateNotFoundBody,
}),
memoryBenchOptions,
)
@@ -182,7 +181,6 @@ describe('memory', () => {
iterations: errorPathsIterations,
buildRequest: buildErrorRequest,
validateResponse: validateErrorResponse,
- validateBody: validateErrorBody,
}),
memoryBenchOptions,
)
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
index c1809ffd98..cbe4449783 100644
--- a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
@@ -69,7 +69,6 @@ describe('memory', () => {
iterations: peakLargePageIterations,
buildRequest: buildPeakLargePageRequest,
validateResponse: validatePeakLargePageResponse,
- validateBody: validatePeakLargePageBody,
}),
memoryBenchOptions,
)
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
index ecfa5bb266..bad7ed132e 100644
--- a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
@@ -76,7 +76,6 @@ describe('memory', () => {
iterations: requestChurnIterations,
buildRequest: buildItemRequest,
validateResponse: validateItemResponse,
- validateBody: validateItemBody,
}),
memoryBenchOptions,
)
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
index 50952fef6c..95f409aa11 100644
--- a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
@@ -98,7 +98,6 @@ describe('memory', () => {
iterations: serializationPayloadIterations,
buildRequest: buildPayloadRequest,
validateResponse: validatePayloadResponse,
- validateBody: validatePayloadBody,
}),
memoryBenchOptions,
)
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
index 1bb5899103..4945ebdaf9 100644
--- a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
@@ -43,7 +43,6 @@ const tssContentTypeFramed = 'application/x-tss-framed'
const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json`
const xTssSerialized = 'x-tss-serialized'
const contextMarker = 'ctx-server-fn-churn'
-const expectedIdsByRequest = new WeakMap()
const commonHeaders = {
'x-tsr-serverFn': 'true',
@@ -186,18 +185,6 @@ function validateEchoedBody(
}
}
-function validateServerFnBody(
- body: string,
- _response: Response,
- request: Request,
-) {
- const expectedId = expectedIdsByRequest.get(request)
-
- if (expectedId) {
- validateEchoedBody(body, request, expectedId)
- }
-}
-
async function assertServerFnChurnSanity(
handler: StartRequestHandler,
urls: FnUrls,
@@ -235,26 +222,15 @@ describe('memory', () => {
if (index % 2 === 0) {
const fixture = getFixtures[fixtureIndex]!
- const request = buildGetRequest(urls.get, fixture)
-
- if (index === 0) {
- expectedIdsByRequest.set(request, fixture.id)
- }
- return request
+ return buildGetRequest(urls.get, fixture)
}
const fixture = postFixtures[fixtureIndex]!
- const request = buildPostRequest(urls.post, fixture)
-
- if (index === 1) {
- expectedIdsByRequest.set(request, fixture.id)
- }
- return request
+ return buildPostRequest(urls.post, fixture)
},
validateResponse: validateServerFnResponse,
- validateBody: validateServerFnBody,
}),
memoryBenchOptions,
)
From b01f4dedd26781f88b32ca15dbc3058d775a90a7 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 11:47:42 +0200
Subject: [PATCH 07/24] cleanup streaming-peak scenario: we only care about
server memory, not client
---
.../streaming-peak/react/memory.bench.ts | 86 ++-----------------
1 file changed, 7 insertions(+), 79 deletions(-)
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
index 8b706c48b0..cf0e2fa5ea 100644
--- a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
@@ -1,8 +1,8 @@
import { bench, describe } from 'vitest'
import {
- createDeterministicRandom,
memoryBenchOptions,
randomSegment,
+ runSequentialRequestLoop,
} from '../../../bench-utils'
import type { StartRequestHandler } from '../../../bench-utils'
@@ -60,23 +60,6 @@ function getResponseReader(response: Response) {
return reader
}
-async function consumeResponseChunks(response: Response) {
- const reader = getResponseReader(response)
- let chunkCount = 0
-
- while (true) {
- const result = await reader.read()
-
- if (result.done) {
- break
- }
-
- chunkCount++
- }
-
- return chunkCount
-}
-
async function readStreamingBody(response: Response) {
const reader = getResponseReader(response)
const decoder = new TextDecoder()
@@ -124,14 +107,6 @@ function assertFallbacksPrecedeDeferredContent(body: string) {
}
}
-function assertBufferedBodyContainsDeferredSections(body: string) {
- for (const marker of deferredSectionMarkers) {
- if (!body.includes(marker)) {
- throw new Error(`Expected buffered body to contain ${marker}`)
- }
- }
-}
-
async function assertStreamingPeakSanity(handler: StartRequestHandler) {
const chunkedRequest = new Request(
'http://localhost/stream/sanity-chunked',
@@ -150,50 +125,6 @@ async function assertStreamingPeakSanity(handler: StartRequestHandler) {
}
assertFallbacksPrecedeDeferredContent(chunked.body)
-
- const bufferedRequest = new Request(
- 'http://localhost/stream/sanity-buffered',
- requestInit,
- )
- const bufferedResponse = await handler.fetch(bufferedRequest)
-
- validateStreamingResponse(bufferedResponse, bufferedRequest)
-
- const bufferedBody = await bufferedResponse.text()
-
- assertBufferedBodyContainsDeferredSections(bufferedBody)
-}
-
-async function runChunkedStreamingLoop(handler: StartRequestHandler) {
- const random = createDeterministicRandom(benchmarkSeed)
- let totalChunkCount = 0
-
- for (let index = 0; index < streamingPeakIterations; index++) {
- const request = buildStreamingRequest(random, index)
- const response = await handler.fetch(request)
-
- validateStreamingResponse(response, request)
- totalChunkCount += await consumeResponseChunks(response)
- }
-
- return totalChunkCount
-}
-
-async function runBufferedStreamingLoop(handler: StartRequestHandler) {
- const random = createDeterministicRandom(benchmarkSeed)
- let totalBodyLength = 0
-
- for (let index = 0; index < streamingPeakIterations; index++) {
- const request = buildStreamingRequest(random, index)
- const response = await handler.fetch(request)
-
- validateStreamingResponse(response, request)
-
- const body = await response.text()
- totalBodyLength += body.length
- }
-
- return totalBodyLength
}
await assertStreamingPeakSanity(handler)
@@ -202,15 +133,12 @@ describe('memory', () => {
bench(
'mem streaming-peak chunked (react)',
async () => {
- await runChunkedStreamingLoop(handler)
- },
- memoryBenchOptions,
- )
-
- bench(
- 'mem streaming-peak buffered (react)',
- async () => {
- await runBufferedStreamingLoop(handler)
+ await runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: streamingPeakIterations,
+ buildRequest: buildStreamingRequest,
+ validateResponse: validateStreamingResponse,
+ })
},
memoryBenchOptions,
)
From fa3cb8a87ab94370e10a6794bed887ae3783b05a Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 14:28:27 +0200
Subject: [PATCH 08/24] use platformatic/flame for local memory bench
---
.gitignore | 3 +
benchmarks/memory/README.md | 53 ++-
benchmarks/memory/client/benchmark.ts | 7 +
benchmarks/memory/client/flame-runner.ts | 18 +
benchmarks/memory/client/jsdom.ts | 51 +++
benchmarks/memory/client/package.json | 54 +++
benchmarks/memory/client/runner.ts | 25 ++
.../react/memory.bench.ts | 48 +--
.../react/memory.flame.ts | 4 +
.../react/project.json | 23 ++
.../interrupted-navigations/react/setup.ts | 23 ++
.../react/tsconfig.json | 1 +
.../react/memory.bench.ts | 45 +--
.../react/memory.flame.ts | 4 +
.../loader-data-retention/react/project.json | 23 ++
.../loader-data-retention/react/setup.ts | 21 +
.../react/src/loader-data.ts | 2 +-
.../loader-data-retention/react/tsconfig.json | 1 +
.../mount-unmount/react/memory.bench.ts | 18 +-
.../mount-unmount/react/memory.flame.ts | 4 +
.../mount-unmount/react/project.json | 23 ++
.../scenarios/mount-unmount/react/setup.ts | 7 +
.../mount-unmount/react/tsconfig.json | 1 +
.../navigation-churn/react/memory.bench.ts | 31 +-
.../navigation-churn/react/memory.flame.ts | 4 +
.../navigation-churn/react/project.json | 23 ++
.../scenarios/navigation-churn/react/setup.ts | 7 +
.../navigation-churn/react/tsconfig.json | 1 +
.../preload-churn/react/memory.bench.ts | 52 +--
.../preload-churn/react/memory.flame.ts | 4 +
.../preload-churn/react/project.json | 23 ++
.../scenarios/preload-churn/react/setup.ts | 27 ++
.../preload-churn/react/tsconfig.json | 1 +
.../react/memory.bench.ts | 43 +--
.../react/memory.flame.ts | 4 +
.../unique-location-churn/react/project.json | 23 ++
.../unique-location-churn/react/setup.ts | 18 +
.../unique-location-churn/react/tsconfig.json | 1 +
.../memory/client/vitest.react.config.ts | 1 +
benchmarks/memory/flame-control.ts | 49 +++
benchmarks/memory/run-flame.mjs | 110 ++++++
benchmarks/memory/server/benchmark.ts | 10 +
benchmarks/memory/server/flame-runner.ts | 11 +
benchmarks/memory/server/package.json | 46 +++
benchmarks/memory/server/runner.ts | 9 +
.../aborted-requests/react/memory.bench.ts | 162 +-------
.../aborted-requests/react/memory.flame.ts | 4 +
.../aborted-requests/react/project.json | 23 ++
.../scenarios/aborted-requests/react/setup.ts | 163 ++++++++
.../aborted-requests/react/tsconfig.json | 3 +
.../error-paths/react/memory.bench.ts | 201 +---------
.../error-paths/react/memory.flame.ts | 4 +
.../scenarios/error-paths/react/project.json | 23 ++
.../scenarios/error-paths/react/setup.ts | 212 ++++++++++
.../scenarios/error-paths/react/tsconfig.json | 3 +
.../peak-large-page/react/memory.bench.ts | 77 +---
.../peak-large-page/react/memory.flame.ts | 4 +
.../peak-large-page/react/project.json | 23 ++
.../scenarios/peak-large-page/react/setup.ts | 76 ++++
.../peak-large-page/react/tsconfig.json | 3 +
.../request-churn/react/memory.bench.ts | 84 +---
.../request-churn/react/memory.flame.ts | 4 +
.../request-churn/react/project.json | 23 ++
.../scenarios/request-churn/react/setup.ts | 85 ++++
.../request-churn/react/tsconfig.json | 3 +
.../react/memory.bench.ts | 106 +----
.../react/memory.flame.ts | 4 +
.../serialization-payload/react/project.json | 23 ++
.../serialization-payload/react/setup.ts | 107 ++++++
.../serialization-payload/react/tsconfig.json | 3 +
.../server-fn-churn/react/memory.bench.ts | 239 +-----------
.../server-fn-churn/react/memory.flame.ts | 4 +
.../server-fn-churn/react/project.json | 23 ++
.../scenarios/server-fn-churn/react/setup.ts | 239 ++++++++++++
.../server-fn-churn/react/tsconfig.json | 3 +
.../streaming-peak/react/memory.bench.ts | 147 +------
.../streaming-peak/react/memory.flame.ts | 4 +
.../streaming-peak/react/project.json | 23 ++
.../scenarios/streaming-peak/react/setup.ts | 147 +++++++
.../streaming-peak/react/tsconfig.json | 3 +
.../memory/server/vitest.react.config.ts | 1 +
package.json | 2 +
pnpm-lock.yaml | 362 +++++++++++++++---
83 files changed, 2335 insertions(+), 1242 deletions(-)
create mode 100644 benchmarks/memory/client/benchmark.ts
create mode 100644 benchmarks/memory/client/flame-runner.ts
create mode 100644 benchmarks/memory/client/jsdom.ts
create mode 100644 benchmarks/memory/client/runner.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts
create mode 100644 benchmarks/memory/flame-control.ts
create mode 100644 benchmarks/memory/run-flame.mjs
create mode 100644 benchmarks/memory/server/benchmark.ts
create mode 100644 benchmarks/memory/server/flame-runner.ts
create mode 100644 benchmarks/memory/server/runner.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
diff --git a/.gitignore b/.gitignore
index 4c5184f4d8..1699592784 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,3 +90,6 @@ vite.config.ts.timestamp_*
# eslint-plugin-start perf fixtures
/e2e/eslint-plugin-start/src/perf/generated
+
+# local memory flame profiles
+benchmarks/memory/**/.profiles/
diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md
index c03f112329..d6c007aa43 100644
--- a/benchmarks/memory/README.md
+++ b/benchmarks/memory/README.md
@@ -18,14 +18,16 @@ be added later without renames.
```text
benchmarks/memory//
- package.json Nx targets: build:react, test:perf:react, test:types
+ package.json Nx targets: build:react, test:perf:react, test:flame:react, test:types
bench-utils.ts memoryBenchOptions, seeded LCG (+ sequential request loop on the server side)
vitest.react.config.ts aggregates scenarios/*/react/vite.config.ts
- scenarios//react/ one isolated app per scenario + memory.bench.ts
+ scenarios//react/ one isolated app per scenario + setup.ts + memory.bench.ts + memory.flame.ts
```
One app per scenario; apps and bench names are stable once landed (CodSpeed
continuity). Never grow an existing scenario for a new case — add a scenario.
+`setup.ts` owns the scenario workload, sanity checks, and any deterministic id
+generation; `memory.bench.ts` and `memory.flame.ts` are thin runners only.
## How the memory instrument executes a bench
@@ -105,6 +107,9 @@ continuity). Never grow an existing scenario for a new case — add a scenario.
## Run
+Smoke-test the CodSpeed/Vitest benchmark entrypoints and typecheck the
+scenarios:
+
```bash
pnpm nx run @benchmarks/memory-server:test:perf:react --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/memory-client:test:perf:react --outputStyle=stream --skipRemoteCache
@@ -112,6 +117,50 @@ pnpm nx run @benchmarks/memory-server:test:types --outputStyle=stream --skipRemo
pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemoteCache
```
+Local attribution profiling, without CodSpeed CLI/login/sudo/upload, uses
+`@platformatic/flame` heap profiles. These targets rebuild the scenarios with
+`--sourcemap true` so the generated profile reports can point back to source;
+the normal CodSpeed benchmark builds are unchanged. Local aggregate scripts run
+with `--parallel=1`, and scenario `test:flame` targets opt out of Nx parallelism
+so profiling workloads do not overlap and bias each other. The Vitest aggregate
+configs also set `fileParallelism: false` so benchmark files run sequentially
+inside `test:perf:react`.
+
+```bash
+pnpm benchmark:memory:server:flame
+pnpm benchmark:memory:client:flame
+```
+
+To profile one scenario, run its `test:flame` target directly:
+
+```bash
+pnpm nx run @benchmarks/memory-server-request-churn-react:test:flame --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-client-navigation-churn-react:test:flame --outputStyle=stream --skipRemoteCache
+```
+
+Flame writes reports under the scenario's ignored `.profiles//`
+directory, including `heap-profile-*.html` and `heap-profile-*.md`. The
+`memory.flame.ts` entrypoints run the same workload shape as `memory.bench.ts`
+but manually start profiling after sanity/setup work and stop it after the
+measured workload. Treat these profiles as diagnostic heap-sampling attribution;
+they are not CodSpeed memory metrics such as peak memory, allocated bytes, or
+allocation counts. Reports can include `@platformatic/flame` and pprof shutdown
+frames. Flame runs do not force GC before profiling; doing so would perturb the
+workload and still would not make heap sampling equivalent to CodSpeed memory
+metrics.
+
+Clean local Flame profile output with:
+
+```bash
+pnpm --filter @benchmarks/memory-server clean:profiles
+pnpm --filter @benchmarks/memory-client clean:profiles
+```
+
+Client memory benches are useful for regression tracking of router/React/jsdom
+integration behavior, especially retained route/cache data. They are not pure
+browser-memory measurements, and local Flame attribution can include jsdom,
+React DOM, and profiler shutdown frames.
+
Real memory measurement, locally (requires the CodSpeed CLI, `codspeed setup`
once to install the memory executor, and sudo; **uploads results to the
CodSpeed dashboard** — local runs do not affect PR baselines):
diff --git a/benchmarks/memory/client/benchmark.ts b/benchmarks/memory/client/benchmark.ts
new file mode 100644
index 0000000000..bcf043c953
--- /dev/null
+++ b/benchmarks/memory/client/benchmark.ts
@@ -0,0 +1,7 @@
+export interface ClientMemoryBenchmark {
+ name: string
+ before?: () => Promise | void
+ run: () => Promise | void
+ sanity: () => Promise | void
+ after?: () => Promise | void
+}
diff --git a/benchmarks/memory/client/flame-runner.ts b/benchmarks/memory/client/flame-runner.ts
new file mode 100644
index 0000000000..504e4cda7d
--- /dev/null
+++ b/benchmarks/memory/client/flame-runner.ts
@@ -0,0 +1,18 @@
+import { profileFlameWorkload } from '../flame-control.ts'
+import { window } from './jsdom.ts'
+import type { ClientMemoryBenchmark } from './benchmark.ts'
+
+export async function runClientFlameBenchmark(
+ setup: () => ClientMemoryBenchmark,
+) {
+ const test = setup()
+
+ try {
+ await setup().sanity()
+ await test.before?.()
+ await profileFlameWorkload(test.run)
+ } finally {
+ await test.after?.()
+ window.close()
+ }
+}
diff --git a/benchmarks/memory/client/jsdom.ts b/benchmarks/memory/client/jsdom.ts
new file mode 100644
index 0000000000..4df480df68
--- /dev/null
+++ b/benchmarks/memory/client/jsdom.ts
@@ -0,0 +1,51 @@
+import { JSDOM } from 'jsdom'
+
+const dom = new JSDOM('', {
+ url: 'http://localhost/',
+})
+
+const { window } = dom
+
+function setGlobal(name: string, value: unknown) {
+ Object.defineProperty(globalThis, name, {
+ value,
+ configurable: true,
+ writable: true,
+ })
+}
+
+setGlobal('window', window)
+setGlobal('document', window.document)
+setGlobal('self', window)
+setGlobal('navigator', window.navigator)
+setGlobal('location', window.location)
+setGlobal('history', window.history)
+setGlobal('HTMLElement', window.HTMLElement)
+setGlobal('Element', window.Element)
+setGlobal('SVGElement', window.SVGElement)
+setGlobal('DocumentFragment', window.DocumentFragment)
+setGlobal('Node', window.Node)
+setGlobal('MouseEvent', window.MouseEvent)
+setGlobal('MutationObserver', window.MutationObserver)
+setGlobal('sessionStorage', window.sessionStorage)
+setGlobal('localStorage', window.localStorage)
+setGlobal('getComputedStyle', window.getComputedStyle.bind(window))
+
+setGlobal(
+ 'requestAnimationFrame',
+ window.requestAnimationFrame?.bind(window) ??
+ ((callback: (time: number) => void) =>
+ setTimeout(() => callback(performance.now()), 16)),
+)
+
+setGlobal(
+ 'cancelAnimationFrame',
+ window.cancelAnimationFrame?.bind(window) ??
+ ((handle: number) => clearTimeout(handle)),
+)
+
+const scrollTo = () => {}
+window.scrollTo = scrollTo
+setGlobal('scrollTo', scrollTo)
+
+export { window }
diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json
index 1bb60b0c1c..3602143c57 100644
--- a/benchmarks/memory/client/package.json
+++ b/benchmarks/memory/client/package.json
@@ -2,6 +2,23 @@
"name": "@benchmarks/memory-client",
"private": true,
"type": "module",
+ "scripts": {
+ "clean:profiles": "rm -rf scenarios/*/*/.profiles"
+ },
+ "imports": {
+ "#memory-client/bench-utils": {
+ "types": "./bench-utils.ts",
+ "default": "./bench-utils.ts"
+ },
+ "#memory-client/flame-runner": {
+ "types": "./flame-runner.ts",
+ "default": "./flame-runner.ts"
+ },
+ "#memory-client/runner": {
+ "types": "./runner.ts",
+ "default": "./runner.ts"
+ }
+ },
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/router-core": "workspace:^",
@@ -10,9 +27,11 @@
},
"devDependencies": {
"@codspeed/vitest-plugin": "^5.5.0",
+ "@platformatic/flame": "^1.6.0",
"@testing-library/react": "^16.2.0",
"@vitejs/plugin-react": "^6.0.1",
"@types/jsdom": "28.0.0",
+ "jsdom": "29.1.1",
"typescript": "^6.0.2",
"vite": "^8.0.14",
"vitest": "^4.1.4"
@@ -36,8 +55,43 @@
}
]
},
+ "build:react:flame": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-react",
+ "@benchmarks/memory-client-unique-location-churn-react",
+ "@benchmarks/memory-client-preload-churn-react",
+ "@benchmarks/memory-client-loader-data-retention-react",
+ "@benchmarks/memory-client-mount-unmount-react",
+ "@benchmarks/memory-client-interrupted-navigations-react"
+ ],
+ "target": "build:client:flame"
+ }
+ ]
+ },
+ "test:flame:react": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-react",
+ "@benchmarks/memory-client-unique-location-churn-react",
+ "@benchmarks/memory-client-preload-churn-react",
+ "@benchmarks/memory-client-loader-data-retention-react",
+ "@benchmarks/memory-client-mount-unmount-react",
+ "@benchmarks/memory-client-interrupted-navigations-react"
+ ],
+ "target": "test:flame"
+ }
+ ]
+ },
"test:perf:react": {
"executor": "nx:run-commands",
+ "parallelism": false,
"cache": false,
"dependsOn": [
"build:react"
diff --git a/benchmarks/memory/client/runner.ts b/benchmarks/memory/client/runner.ts
new file mode 100644
index 0000000000..a3f29d732a
--- /dev/null
+++ b/benchmarks/memory/client/runner.ts
@@ -0,0 +1,25 @@
+import { afterAll, beforeAll, bench } from 'vitest'
+import { memoryBenchOptions } from './bench-utils'
+import type { ClientMemoryBenchmark } from './benchmark'
+
+export function registerClientMemoryBench(test: ClientMemoryBenchmark) {
+ if (test.before && test.after) {
+ /**
+ * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
+ * honors tinybench's setup/teardown options; the CodSpeed runner does the
+ * exact opposite. Both registrations are load-bearing: exactly one pair
+ * runs in any given mode.
+ */
+ beforeAll(test.before)
+ afterAll(test.after)
+
+ bench(test.name, test.run, {
+ ...memoryBenchOptions,
+ setup: test.before,
+ teardown: test.after,
+ })
+ return
+ }
+
+ bench(test.name, test.run, memoryBenchOptions)
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts
index bc4c8ca6cd..5b1c779d71 100644
--- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts
@@ -1,51 +1,9 @@
-import { afterAll, beforeAll, bench, describe } from 'vitest'
-import {
- createDeterministicRandom,
- memoryBenchOptions,
- randomSegment,
-} from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
import { setup } from './setup'
-const interruptedNavigationIterations = 150
-
-const interruptedNavigationPairs = createInterruptedNavigationPairs(
- interruptedNavigationIterations,
-)
-
-function createInterruptedNavigationPairs(iterations: number) {
- const random = createDeterministicRandom(13)
-
- return Array.from({ length: iterations }, (_, index) => ({
- slowId: `slow-${index}-${randomSegment(random)}`,
- fastId: `fast-${index}-${randomSegment(random)}`,
- }))
-}
-
await setup().sanity()
describe('memory', () => {
- const test = setup()
-
- /**
- * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
- * honors tinybench's setup/teardown options; the CodSpeed runner does the
- * exact opposite. Both registrations are load-bearing — exactly one pair
- * runs in any given mode.
- */
- beforeAll(test.before)
- afterAll(test.after)
-
- bench(
- 'mem interrupted-navigations (react)',
- async () => {
- for (const pair of interruptedNavigationPairs) {
- await test.interrupt(pair.slowId, pair.fastId)
- }
- },
- {
- ...memoryBenchOptions,
- setup: test.before,
- teardown: test.after,
- },
- )
+ registerClientMemoryBench(setup())
})
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json
index 77ccf8d85a..0ed437c9ca 100644
--- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:client": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
index 4cc8182b05..320d4cfb0b 100644
--- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
@@ -1,4 +1,8 @@
import type * as App from './src/app'
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
type NavigationSettlement =
| {
@@ -17,6 +21,10 @@ const {
resolveSlowLoader,
slowLoaderRegistry,
} = (await import(/* @vite-ignore */ appModulePath)) as typeof App
+const interruptedNavigationIterations = 150
+const interruptedNavigationPairs = createInterruptedNavigationPairs(
+ interruptedNavigationIterations,
+)
const uninitialized = () =>
Promise.reject(
@@ -29,6 +37,15 @@ const uninitializedSettlement = () =>
reason: new Error('interrupted-navigations benchmark is not initialized'),
})
+function createInterruptedNavigationPairs(iterations: number) {
+ const random = createDeterministicRandom(13)
+
+ return Array.from({ length: iterations }, (_, index) => ({
+ slowId: `slow-${index}-${randomSegment(random)}`,
+ fastId: `fast-${index}-${randomSegment(random)}`,
+ }))
+}
+
async function drainMicrotasks() {
await Promise.resolve()
await Promise.resolve()
@@ -245,8 +262,14 @@ export function setup() {
}
return {
+ name: 'mem interrupted-navigations (react)',
before,
interrupt,
+ async run() {
+ for (const pair of interruptedNavigationPairs) {
+ await interrupt(pair.slowId, pair.fastId)
+ }
+ },
async sanity() {
await before()
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json
index 26767bd491..ea566061d9 100644
--- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json
@@ -10,6 +10,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"memory.bench.ts",
+ "memory.flame.ts",
"setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts
index 331c74fb2a..5b1c779d71 100644
--- a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts
@@ -1,48 +1,9 @@
-import { afterAll, beforeAll, bench, describe } from 'vitest'
-import {
- createDeterministicRandom,
- memoryBenchOptions,
- randomSegment,
-} from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
import { setup } from './setup'
-const loaderDataRetentionNavigationCount = 20
-const pageIds = createPageIds()
-
await setup().sanity()
describe('memory', () => {
- const test = setup()
-
- /**
- * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
- * honors tinybench's setup/teardown options; the CodSpeed runner does the
- * exact opposite. Both registrations are load-bearing — exactly one pair
- * runs in any given mode.
- */
- beforeAll(test.before)
- afterAll(test.after)
-
- bench(
- 'mem loader-data-retention (react)',
- async () => {
- for (const id of pageIds) {
- await test.navigate(id)
- }
- },
- {
- ...memoryBenchOptions,
- setup: test.before,
- teardown: test.after,
- },
- )
+ registerClientMemoryBench(setup())
})
-
-function createPageIds() {
- const random = createDeterministicRandom(11)
-
- return Array.from(
- { length: loaderDataRetentionNavigationCount },
- (_, index) => `${index}-${randomSegment(random)}`,
- )
-}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json
index 953558d3f6..713038e20f 100644
--- a/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:client": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
index 3459edec5a..934f37c1bf 100644
--- a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
@@ -1,15 +1,30 @@
import type * as App from './src/app'
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
const appModulePath = './dist/app.js'
const { loaderPayloadRecordCount, mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
+const loaderDataRetentionNavigationCount = 20
+const pageIds = createPageIds()
const uninitialized = () =>
Promise.reject(
new Error('loader-data-retention benchmark is not initialized'),
)
+function createPageIds() {
+ const random = createDeterministicRandom(11)
+
+ return Array.from(
+ { length: loaderDataRetentionNavigationCount },
+ (_, index) => `${index}-${randomSegment(random)}`,
+ )
+}
+
export function setup() {
if (process.env.NODE_ENV !== 'production') {
console.warn(
@@ -129,8 +144,14 @@ export function setup() {
}
return {
+ name: 'mem loader-data-retention (react)',
before,
navigate: (id: string) => navigateTo(id),
+ async run() {
+ for (const id of pageIds) {
+ await navigateTo(id)
+ }
+ },
async sanity() {
await before()
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts
index d2a73a6542..3bd59c82ef 100644
--- a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts
@@ -1,7 +1,7 @@
import {
createDeterministicRandom,
randomSegment,
-} from '../../../../bench-utils'
+} from '#memory-client/bench-utils'
export const loaderPayloadRecordCount = 512
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json
index 26767bd491..ea566061d9 100644
--- a/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json
@@ -10,6 +10,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"memory.bench.ts",
+ "memory.flame.ts",
"setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts
index 042109dd19..5b1c779d71 100644
--- a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts
@@ -1,21 +1,9 @@
-import { bench, describe } from 'vitest'
-import { memoryBenchOptions } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
import { setup } from './setup'
-const mountUnmountIterations = 100
-
await setup().sanity()
describe('memory', () => {
- const test = setup()
-
- bench(
- 'mem mount-unmount (react)',
- async () => {
- for (let index = 0; index < mountUnmountIterations; index++) {
- await test.cycle()
- }
- },
- memoryBenchOptions,
- )
+ registerClientMemoryBench(setup())
})
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/project.json b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json
index f701b92e2a..05d8aaa4bf 100644
--- a/benchmarks/memory/client/scenarios/mount-unmount/react/project.json
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:client": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
index 3ba792e11a..203dc39d37 100644
--- a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
@@ -4,6 +4,7 @@ const appModulePath = './dist/app.js'
const { mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
+const mountUnmountIterations = 100
function drainMicrotasks() {
return Promise.resolve().then(() => Promise.resolve())
@@ -55,7 +56,13 @@ export function setup() {
}
return {
+ name: 'mem mount-unmount (react)',
cycle,
+ async run() {
+ for (let index = 0; index < mountUnmountIterations; index++) {
+ await cycle()
+ }
+ },
async sanity() {
assertEmptyBody()
await cycle()
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json
index 26767bd491..ea566061d9 100644
--- a/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json
@@ -10,6 +10,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"memory.bench.ts",
+ "memory.flame.ts",
"setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts
index 7e2fe82027..5b1c779d71 100644
--- a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts
@@ -1,34 +1,9 @@
-import { afterAll, beforeAll, bench, describe } from 'vitest'
-import { memoryBenchOptions } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
import { setup } from './setup'
-const navigationChurnIterations = 300
-
await setup().sanity()
describe('memory', () => {
- const test = setup()
-
- /**
- * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
- * honors tinybench's setup/teardown options; the CodSpeed runner does the
- * exact opposite. Both registrations are load-bearing — exactly one pair
- * runs in any given mode.
- */
- beforeAll(test.before)
- afterAll(test.after)
-
- bench(
- 'mem navigation-churn (react)',
- async () => {
- for (let index = 0; index < navigationChurnIterations; index++) {
- await test.navigate(index % 2 === 0 ? '/b' : '/a')
- }
- },
- {
- ...memoryBenchOptions,
- setup: test.before,
- teardown: test.after,
- },
- )
+ registerClientMemoryBench(setup())
})
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/project.json b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json
index 1409a41b61..b424277bc6 100644
--- a/benchmarks/memory/client/scenarios/navigation-churn/react/project.json
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:client": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
index f31c63c360..deac1d1bbf 100644
--- a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
@@ -6,6 +6,7 @@ const appModulePath = './dist/app.js'
const { mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
+const navigationChurnIterations = 300
const uninitialized = () =>
Promise.reject(new Error('navigation-churn benchmark is not initialized'))
@@ -99,8 +100,14 @@ export function setup() {
}
return {
+ name: 'mem navigation-churn (react)',
before,
navigate: (target: Target) => navigateTo(target),
+ async run() {
+ for (let index = 0; index < navigationChurnIterations; index++) {
+ await navigateTo(index % 2 === 0 ? '/b' : '/a')
+ }
+ },
async sanity() {
await before()
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json
index 26767bd491..ea566061d9 100644
--- a/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json
@@ -10,6 +10,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"memory.bench.ts",
+ "memory.flame.ts",
"setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts
index 499fb8461b..5b1c779d71 100644
--- a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts
@@ -1,55 +1,9 @@
-import { afterAll, beforeAll, bench, describe } from 'vitest'
-import {
- createDeterministicRandom,
- memoryBenchOptions,
- randomSegment,
-} from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
import { setup } from './setup'
-const preloadChurnIterations = 200
-// A navigation commit is what triggers the router's clearExpiredCache —
-// preloaded matches (defaultPreloadGcTime: 0) are only evicted then, never
-// during a preload-only loop. Interleaving a navigation every few preloads is
-// what makes the flat floor assert "eviction releases preloaded payloads".
-const preloadsPerEvictionNavigation = 10
-// Module-level so ids stay unique across the CodSpeed runner's multiple
-// invocations (warmup + measured) of the bench fn on one mount; a
-// per-invocation LCG would replay identical ids, and every preload after the
-// first invocation would dedupe against cachedMatches instead of doing work.
-const benchmarkRandom = createDeterministicRandom(0x706c6f61)
-let preloadCounter = 0
-
await setup().sanity()
describe('memory', () => {
- const test = setup()
-
- /**
- * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
- * honors tinybench's setup/teardown options; the CodSpeed runner does the
- * exact opposite. Both registrations are load-bearing — exactly one pair
- * runs in any given mode.
- */
- beforeAll(test.before)
- afterAll(test.after)
-
- bench(
- 'mem preload-churn (react)',
- async () => {
- for (let index = 0; index < preloadChurnIterations; index++) {
- await test.preload(
- `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`,
- )
-
- if ((index + 1) % preloadsPerEvictionNavigation === 0) {
- await test.evictPreloads()
- }
- }
- },
- {
- ...memoryBenchOptions,
- setup: test.before,
- teardown: test.after,
- },
- )
+ registerClientMemoryBench(setup())
})
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/project.json b/benchmarks/memory/client/scenarios/preload-churn/react/project.json
index 33c837bc6c..238d98c816 100644
--- a/benchmarks/memory/client/scenarios/preload-churn/react/project.json
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:client": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
index 34c2e51558..a1737c644e 100644
--- a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
@@ -1,4 +1,8 @@
import type * as App from './src/app'
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
const appModulePath = './dist/app.js'
const { getTrackedItemLoaderCount, mountTestApp } = (await import(
@@ -10,6 +14,17 @@ type MountedApp = ReturnType
// Fixed id for the eviction navigations interleaved into the bench loop; its
// payload is a constant-size steady-state resident, never part of the signal.
const evictionItemId = 'nav-evict'
+const preloadChurnIterations = 200
+// A navigation commit is what triggers the router's clearExpiredCache --
+// preloaded matches (defaultPreloadGcTime: 0) are only evicted then, never
+// during a preload-only loop. Interleaving a navigation every few preloads is
+// what makes the flat floor assert "eviction releases preloaded payloads".
+const preloadsPerEvictionNavigation = 10
+// Module-level so ids stay unique across runner invocations on one mount; a
+// per-invocation LCG would replay identical ids, and every preload after the
+// first invocation would dedupe against cachedMatches instead of doing work.
+const benchmarkRandom = createDeterministicRandom(0x706c6f61)
+let preloadCounter = 0
const uninitialized = async (_id: string) => {
throw new Error('preload-churn benchmark is not initialized')
@@ -184,9 +199,21 @@ export function setup() {
}
return {
+ name: 'mem preload-churn (react)',
before,
preload: (id: string) => preloadItem(id),
evictPreloads,
+ async run() {
+ for (let index = 0; index < preloadChurnIterations; index++) {
+ await preloadItem(
+ `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`,
+ )
+
+ if ((index + 1) % preloadsPerEvictionNavigation === 0) {
+ await evictPreloads()
+ }
+ }
+ },
async sanity() {
await before()
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json
index 26767bd491..ea566061d9 100644
--- a/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json
@@ -10,6 +10,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"memory.bench.ts",
+ "memory.flame.ts",
"setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts
index db56f30d8d..5b1c779d71 100644
--- a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts
@@ -1,46 +1,9 @@
-import { afterAll, beforeAll, bench, describe } from 'vitest'
-import {
- createDeterministicRandom,
- memoryBenchOptions,
- randomSegment,
-} from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
import { setup } from './setup'
-const benchmarkRandom = createDeterministicRandom(0xdecafbad)
-const uniqueLocationChurnIterations = 300
-// Module-level so ids stay unique across the CodSpeed runner's multiple
-// invocations (warmup + measured) of the bench fn on one mount; the counter
-// prefix removes any residual LCG birthday-collision risk.
-let locationCounter = 0
-
await setup().sanity()
describe('memory', () => {
- const test = setup()
-
- /**
- * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only
- * honors tinybench's setup/teardown options; the CodSpeed runner does the
- * exact opposite. Both registrations are load-bearing — exactly one pair
- * runs in any given mode.
- */
- beforeAll(test.before)
- afterAll(test.after)
-
- bench(
- 'mem unique-location-churn (react)',
- async () => {
- for (let index = 0; index < uniqueLocationChurnIterations; index++) {
- const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`
- const q = `q-${randomSegment(benchmarkRandom)}`
-
- await test.navigate({ id, q })
- }
- },
- {
- ...memoryBenchOptions,
- setup: test.before,
- teardown: test.after,
- },
- )
+ registerClientMemoryBench(setup())
})
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json
index db7c57b1a3..1572552650 100644
--- a/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:client": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
index 6f58a1281a..14fbfe7081 100644
--- a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
@@ -1,4 +1,8 @@
import type * as App from './src/app'
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
type ItemLocation = {
id: string
@@ -9,6 +13,11 @@ const appModulePath = './dist/app.js'
const { mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
+const benchmarkRandom = createDeterministicRandom(0xdecafbad)
+const uniqueLocationChurnIterations = 300
+// Module-level so ids stay unique across runner invocations on one mount; the
+// counter prefix removes any residual LCG birthday-collision risk.
+let locationCounter = 0
const uninitialized = () =>
Promise.reject(
@@ -104,8 +113,17 @@ export function setup() {
}
return {
+ name: 'mem unique-location-churn (react)',
before,
navigate: (location: ItemLocation) => navigateTo(location),
+ async run() {
+ for (let index = 0; index < uniqueLocationChurnIterations; index++) {
+ const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`
+ const q = `q-${randomSegment(benchmarkRandom)}`
+
+ await navigateTo({ id, q })
+ }
+ },
async sanity() {
await before()
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json
index 26767bd491..ea566061d9 100644
--- a/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json
@@ -10,6 +10,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"memory.bench.ts",
+ "memory.flame.ts",
"setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
diff --git a/benchmarks/memory/client/vitest.react.config.ts b/benchmarks/memory/client/vitest.react.config.ts
index 356ab7b3a8..44643b4d60 100644
--- a/benchmarks/memory/client/vitest.react.config.ts
+++ b/benchmarks/memory/client/vitest.react.config.ts
@@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
watch: false,
+ fileParallelism: false,
projects: ['./scenarios/*/react/vite.config.ts'],
},
})
diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts
new file mode 100644
index 0000000000..e3aa127033
--- /dev/null
+++ b/benchmarks/memory/flame-control.ts
@@ -0,0 +1,49 @@
+const flameEnabled = process.env.TSR_MEMORY_FLAME === '1'
+const flameStartDelayMs = 250
+
+function wait(ms: number) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms)
+ })
+}
+
+function waitForSignalHandler() {
+ return wait(0)
+}
+
+async function toggleFlameProfile() {
+ if (!flameEnabled) {
+ return
+ }
+
+ if (process.platform === 'win32') {
+ throw new Error('Flame manual profiling is not supported on Windows')
+ }
+
+ process.kill(process.pid, 'SIGUSR2')
+ await waitForSignalHandler()
+}
+
+export async function startFlameProfile() {
+ if (flameEnabled) {
+ // Flame initializes sourcemap support asynchronously when the process starts.
+ await wait(flameStartDelayMs)
+ }
+
+ await toggleFlameProfile()
+}
+
+export async function stopFlameProfile() {
+ await toggleFlameProfile()
+}
+
+export async function profileFlameWorkload(
+ workload: () => Promise | void,
+) {
+ await startFlameProfile()
+ try {
+ await workload()
+ } finally {
+ await stopFlameProfile()
+ }
+}
diff --git a/benchmarks/memory/run-flame.mjs b/benchmarks/memory/run-flame.mjs
new file mode 100644
index 0000000000..f999be5340
--- /dev/null
+++ b/benchmarks/memory/run-flame.mjs
@@ -0,0 +1,110 @@
+#!/usr/bin/env node
+import fs from 'node:fs'
+import { createRequire } from 'node:module'
+import path from 'node:path'
+import process from 'node:process'
+
+const [, , entrypointArg, sourcemapDirArg] = process.argv
+
+if (!entrypointArg || !sourcemapDirArg) {
+ console.error(
+ 'Usage: node benchmarks/memory/run-flame.mjs ',
+ )
+ process.exit(1)
+}
+
+const entrypointPath = path.resolve(entrypointArg)
+const sourcemapDirPath = path.resolve(sourcemapDirArg)
+
+if (!fs.existsSync(entrypointPath)) {
+ console.error(`Flame entrypoint not found: ${entrypointPath}`)
+ process.exit(1)
+}
+
+if (!fs.existsSync(sourcemapDirPath)) {
+ console.error(`Flame sourcemap directory not found: ${sourcemapDirPath}`)
+ process.exit(1)
+}
+
+const profileDir = path.join(
+ path.dirname(entrypointPath),
+ '.profiles',
+ new Date().toISOString().replace(/[:.]/g, '-'),
+)
+
+fs.mkdirSync(profileDir, { recursive: true })
+
+const entrypointRequire = createRequire(entrypointPath)
+const { generateFlamegraph, generateMarkdown, startProfiling } =
+ entrypointRequire('@platformatic/flame')
+
+process.env.NODE_ENV = 'production'
+process.env.TSR_MEMORY_FLAME = '1'
+
+console.log(`Flame profile directory: ${profileDir}`)
+
+const { pid, process: childProcess } = startProfiling(entrypointPath, [], {
+ autoStart: false,
+ cwd: profileDir,
+ mdFormat: 'detailed',
+ sourcemapDirs: [sourcemapDirPath],
+})
+
+console.log(`Flame profiling process: ${pid}`)
+
+const childCode = await new Promise((resolve) => {
+ childProcess.on('error', (error) => {
+ console.error(`Failed to start Flame profiled process: ${error.message}`)
+ resolve(1)
+ })
+
+ childProcess.on('close', (code, signal) => {
+ if (signal) {
+ console.error(`Flame profiled process exited via signal ${signal}`)
+ resolve(1)
+ return
+ }
+
+ resolve(code ?? 0)
+ })
+})
+
+const heapProfilePaths = fs
+ .readdirSync(profileDir)
+ .filter((fileName) => /^heap-profile-.*\.pb$/.test(fileName))
+ .sort()
+ .map((fileName) => path.join(profileDir, fileName))
+
+for (const heapProfilePath of heapProfilePaths) {
+ const htmlPath = heapProfilePath.replace(/\.pb$/, '.html')
+ const mdPath = heapProfilePath.replace(/\.pb$/, '.md')
+
+ console.log(`Generating heap flamegraph: ${htmlPath}`)
+ try {
+ await generateFlamegraph(heapProfilePath, htmlPath)
+ } catch (error) {
+ console.warn(`Failed to generate heap flamegraph: ${error.message}`)
+ }
+
+ console.log(`Generating heap markdown: ${mdPath}`)
+ try {
+ await generateMarkdown(heapProfilePath, mdPath, { format: 'detailed' })
+ } catch (error) {
+ console.warn(`Failed to generate heap markdown: ${error.message}`)
+ }
+}
+
+const heapReportPaths = fs
+ .readdirSync(profileDir)
+ .filter((fileName) => /^heap-profile-.*\.(?:html|md)$/.test(fileName))
+ .sort()
+ .map((fileName) => path.join(profileDir, fileName))
+
+if (heapReportPaths.length > 0) {
+ console.log('Generated heap profile reports:')
+ for (const heapReportPath of heapReportPaths) {
+ console.log(` ${heapReportPath}`)
+ }
+}
+
+process.exit(childCode)
diff --git a/benchmarks/memory/server/benchmark.ts b/benchmarks/memory/server/benchmark.ts
new file mode 100644
index 0000000000..714b16baa4
--- /dev/null
+++ b/benchmarks/memory/server/benchmark.ts
@@ -0,0 +1,10 @@
+export interface ServerMemoryBench {
+ name: string
+ run: () => Promise | void
+}
+
+export interface ServerMemoryBenchmark {
+ sanity: () => Promise | void
+ run: () => Promise | void
+ benches: Array
+}
diff --git a/benchmarks/memory/server/flame-runner.ts b/benchmarks/memory/server/flame-runner.ts
new file mode 100644
index 0000000000..d2a3425b73
--- /dev/null
+++ b/benchmarks/memory/server/flame-runner.ts
@@ -0,0 +1,11 @@
+import { profileFlameWorkload } from '../flame-control.ts'
+import type { ServerMemoryBenchmark } from './benchmark.ts'
+
+export async function runServerFlameBenchmark(
+ setup: () => Promise,
+) {
+ const test = await setup()
+
+ await test.sanity()
+ await profileFlameWorkload(test.run)
+}
diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json
index 5e4273c30b..11ec470bd9 100644
--- a/benchmarks/memory/server/package.json
+++ b/benchmarks/memory/server/package.json
@@ -2,6 +2,14 @@
"name": "@benchmarks/memory-server",
"private": true,
"type": "module",
+ "scripts": {
+ "clean:profiles": "rm -rf scenarios/*/*/.profiles"
+ },
+ "imports": {
+ "#memory-server/bench-utils": "./bench-utils.ts",
+ "#memory-server/flame-runner": "./flame-runner.ts",
+ "#memory-server/runner": "./runner.ts"
+ },
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
@@ -10,6 +18,7 @@
},
"devDependencies": {
"@codspeed/vitest-plugin": "^5.5.0",
+ "@platformatic/flame": "^1.6.0",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.2",
"vite": "^8.0.14",
@@ -35,8 +44,45 @@
}
]
},
+ "build:react:flame": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-react",
+ "@benchmarks/memory-server-server-fn-churn-react",
+ "@benchmarks/memory-server-error-paths-react",
+ "@benchmarks/memory-server-aborted-requests-react",
+ "@benchmarks/memory-server-peak-large-page-react",
+ "@benchmarks/memory-server-streaming-peak-react",
+ "@benchmarks/memory-server-serialization-payload-react"
+ ],
+ "target": "build:ssr:flame"
+ }
+ ]
+ },
+ "test:flame:react": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-react",
+ "@benchmarks/memory-server-server-fn-churn-react",
+ "@benchmarks/memory-server-error-paths-react",
+ "@benchmarks/memory-server-aborted-requests-react",
+ "@benchmarks/memory-server-peak-large-page-react",
+ "@benchmarks/memory-server-streaming-peak-react",
+ "@benchmarks/memory-server-serialization-payload-react"
+ ],
+ "target": "test:flame"
+ }
+ ]
+ },
"test:perf:react": {
"executor": "nx:run-commands",
+ "parallelism": false,
"cache": false,
"dependsOn": [
"build:react"
diff --git a/benchmarks/memory/server/runner.ts b/benchmarks/memory/server/runner.ts
new file mode 100644
index 0000000000..783facbf3b
--- /dev/null
+++ b/benchmarks/memory/server/runner.ts
@@ -0,0 +1,9 @@
+import { bench } from 'vitest'
+import { memoryBenchOptions } from './bench-utils'
+import type { ServerMemoryBenchmark } from './benchmark'
+
+export function registerServerMemoryBenches(test: ServerMemoryBenchmark) {
+ for (const memoryBench of test.benches) {
+ bench(memoryBench.name, memoryBench.run, memoryBenchOptions)
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts
index b800cb974f..1dc2e0da86 100644
--- a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts
@@ -1,160 +1,10 @@
-import { bench, describe } from 'vitest'
-import { memoryBenchOptions } from '../../../bench-utils'
-import type { StartRequestHandler } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
-const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const abortedRequestIterations = 100
-const eagerMarker = 'data-bench="aborted-requests-eager"'
-const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"'
-const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"'
-const alphaFirstRecord = (id: string) => `deferred-alpha-${id}-0`
-const alphaLastRecord = (id: string) => `deferred-alpha-${id}-19`
-const betaFirstRecord = (id: string) => `deferred-beta-${id}-0`
-const betaLastRecord = (id: string) => `deferred-beta-${id}-19`
-
-const textDecoder = new TextDecoder()
-const documentRequestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-const { default: handler } = (await import(
- /* @vite-ignore */ appModuleUrl
-)) as {
- default: StartRequestHandler
-}
-
-function buildStreamRequest(id: string, signal?: AbortSignal) {
- const init: RequestInit = { ...documentRequestInit }
-
- if (signal) {
- init.signal = signal
- }
-
- return new Request(`http://localhost/stream/${id}`, init)
-}
-
-function validateDocumentResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-async function readFirstChunk(response: Response, request: Request) {
- if (!response.body) {
- throw new Error(`Expected response body for ${request.url}`)
- }
-
- const reader = response.body.getReader()
- const result = await reader.read()
- const value = result.value
-
- if (result.done || !value || value.byteLength === 0) {
- await reader.cancel()
- throw new Error(`Expected a non-empty first chunk for ${request.url}`)
- }
-
- return {
- reader,
- value,
- }
-}
-
-async function drainCancellationMicrotasks() {
- await Promise.resolve()
- await Promise.resolve()
-}
-
-async function assertAbortedRequestsSanity(handler: StartRequestHandler) {
- const fullId = 'sanity-full'
- const fullRequest = buildStreamRequest(fullId)
- const fullResponse = await handler.fetch(fullRequest)
- validateDocumentResponse(fullResponse, fullRequest)
-
- const fullBody = await fullResponse.text()
-
- if (!fullBody.includes(eagerMarker)) {
- throw new Error('Expected full sanity response to include the eager marker')
- }
-
- for (const marker of [
- alphaFirstRecord(fullId),
- alphaLastRecord(fullId),
- betaFirstRecord(fullId),
- betaLastRecord(fullId),
- ]) {
- if (!fullBody.includes(marker)) {
- throw new Error(`Expected full sanity response to include ${marker}`)
- }
- }
-
- const midStreamId = 'sanity-mid-stream'
- const controller = new AbortController()
- const midStreamRequest = buildStreamRequest(midStreamId, controller.signal)
- const midStreamResponse = await handler.fetch(midStreamRequest)
- validateDocumentResponse(midStreamResponse, midStreamRequest)
-
- const { reader, value } = await readFirstChunk(
- midStreamResponse,
- midStreamRequest,
- )
- const text = textDecoder.decode(value)
-
- if (!text.includes(eagerMarker)) {
- throw new Error('Expected first sanity chunk to include the eager marker')
- }
-
- if (
- !text.includes(alphaFallbackMarker) ||
- !text.includes(betaFallbackMarker)
- ) {
- throw new Error('Expected first sanity chunk to include deferred fallbacks')
- }
-
- for (const marker of [
- alphaFirstRecord(midStreamId),
- alphaLastRecord(midStreamId),
- betaFirstRecord(midStreamId),
- betaLastRecord(midStreamId),
- ]) {
- if (text.includes(marker)) {
- throw new Error(
- `First sanity chunk already included deferred content ${marker}`,
- )
- }
- }
-
- // reader.cancel() is the response-stream cancellation path if the handler
- // does not observe Request.signal for this in-process request.
- controller.abort()
- await reader.cancel()
- await drainCancellationMicrotasks()
-}
-
-async function runAbortedRequestLoop(handler: StartRequestHandler) {
- for (let index = 0; index < abortedRequestIterations; index++) {
- const controller = new AbortController()
- const request = buildStreamRequest(`abort-${index}`, controller.signal)
- const response = await handler.fetch(request)
- validateDocumentResponse(response, request)
-
- const { reader } = await readFirstChunk(response, request)
- controller.abort()
- await reader.cancel()
- await drainCancellationMicrotasks()
- }
-}
-
-await assertAbortedRequestsSanity(handler)
+const test = await setup()
+await test.sanity()
describe('memory', () => {
- bench(
- 'mem aborted-requests (react)',
- () => runAbortedRequestLoop(handler),
- memoryBenchOptions,
- )
+ registerServerMemoryBenches(test)
})
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/project.json b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json
index c962404c09..bea90c8766 100644
--- a/benchmarks/memory/server/scenarios/aborted-requests/react/project.json
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:ssr": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
new file mode 100644
index 0000000000..404b8410c2
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
@@ -0,0 +1,163 @@
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const abortedRequestIterations = 100
+const eagerMarker = 'data-bench="aborted-requests-eager"'
+const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"'
+const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"'
+const alphaFirstRecord = (id: string) => `deferred-alpha-${id}-0`
+const alphaLastRecord = (id: string) => `deferred-alpha-${id}-19`
+const betaFirstRecord = (id: string) => `deferred-beta-${id}-0`
+const betaLastRecord = (id: string) => `deferred-beta-${id}-19`
+
+const textDecoder = new TextDecoder()
+const documentRequestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildStreamRequest(id: string, signal?: AbortSignal) {
+ const init: RequestInit = { ...documentRequestInit }
+
+ if (signal) {
+ init.signal = signal
+ }
+
+ return new Request(`http://localhost/stream/${id}`, init)
+}
+
+function validateDocumentResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+async function readFirstChunk(response: Response, request: Request) {
+ if (!response.body) {
+ throw new Error(`Expected response body for ${request.url}`)
+ }
+
+ const reader = response.body.getReader()
+ const result = await reader.read()
+ const value = result.value
+
+ if (result.done || !value || value.byteLength === 0) {
+ await reader.cancel()
+ throw new Error(`Expected a non-empty first chunk for ${request.url}`)
+ }
+
+ return {
+ reader,
+ value,
+ }
+}
+
+async function drainCancellationMicrotasks() {
+ await Promise.resolve()
+ await Promise.resolve()
+}
+
+async function assertAbortedRequestsSanity(handler: StartRequestHandler) {
+ const fullId = 'sanity-full'
+ const fullRequest = buildStreamRequest(fullId)
+ const fullResponse = await handler.fetch(fullRequest)
+ validateDocumentResponse(fullResponse, fullRequest)
+
+ const fullBody = await fullResponse.text()
+
+ if (!fullBody.includes(eagerMarker)) {
+ throw new Error('Expected full sanity response to include the eager marker')
+ }
+
+ for (const marker of [
+ alphaFirstRecord(fullId),
+ alphaLastRecord(fullId),
+ betaFirstRecord(fullId),
+ betaLastRecord(fullId),
+ ]) {
+ if (!fullBody.includes(marker)) {
+ throw new Error(`Expected full sanity response to include ${marker}`)
+ }
+ }
+
+ const midStreamId = 'sanity-mid-stream'
+ const controller = new AbortController()
+ const midStreamRequest = buildStreamRequest(midStreamId, controller.signal)
+ const midStreamResponse = await handler.fetch(midStreamRequest)
+ validateDocumentResponse(midStreamResponse, midStreamRequest)
+
+ const { reader, value } = await readFirstChunk(
+ midStreamResponse,
+ midStreamRequest,
+ )
+ const text = textDecoder.decode(value)
+
+ if (!text.includes(eagerMarker)) {
+ throw new Error('Expected first sanity chunk to include the eager marker')
+ }
+
+ if (
+ !text.includes(alphaFallbackMarker) ||
+ !text.includes(betaFallbackMarker)
+ ) {
+ throw new Error('Expected first sanity chunk to include deferred fallbacks')
+ }
+
+ for (const marker of [
+ alphaFirstRecord(midStreamId),
+ alphaLastRecord(midStreamId),
+ betaFirstRecord(midStreamId),
+ betaLastRecord(midStreamId),
+ ]) {
+ if (text.includes(marker)) {
+ throw new Error(
+ `First sanity chunk already included deferred content ${marker}`,
+ )
+ }
+ }
+
+ // reader.cancel() is the response-stream cancellation path if the handler
+ // does not observe Request.signal for this in-process request.
+ controller.abort()
+ await reader.cancel()
+ await drainCancellationMicrotasks()
+}
+
+async function runAbortedRequestLoop(handler: StartRequestHandler) {
+ for (let index = 0; index < abortedRequestIterations; index++) {
+ const controller = new AbortController()
+ const request = buildStreamRequest(`abort-${index}`, controller.signal)
+ const response = await handler.fetch(request)
+ validateDocumentResponse(response, request)
+
+ const { reader } = await readFirstChunk(response, request)
+ controller.abort()
+ await reader.cancel()
+ await drainCancellationMicrotasks()
+ }
+}
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ const run = () => runAbortedRequestLoop(handler)
+
+ return {
+ sanity: () => assertAbortedRequestsSanity(handler),
+ run,
+ benches: [
+ {
+ name: 'mem aborted-requests (react)',
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json
index 4d78884cea..11ddcce4ea 100644
--- a/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json
@@ -1,12 +1,15 @@
{
"extends": "../../../../../../tsconfig.json",
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": [
"memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
"./src/**/*"
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
index bbbfd63456..1dc2e0da86 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts
@@ -1,199 +1,10 @@
-import { bench, describe } from 'vitest'
-import {
- memoryBenchOptions,
- randomSegment,
- runSequentialRequestLoop,
-} from '../../../bench-utils'
-import type { StartRequestHandler } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
-const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const errorPathsIterations = 50
-const redirectSeed = 0xdecafbad
-const notFoundSeed = 0xdecafb0d
-const errorSeed = 0xdecafbed
-const unmatchedSeed = 0xdecaf00d
-
-const redirectStatus = 302
-const notFoundStatus = 404
-const errorStatus = 500
-const notFoundMarker = 'data-bench="not-found-boundary"'
-const errorMarker = 'data-bench="error-boundary"'
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-const { default: handler } = (await import(
- /* @vite-ignore */ appModuleUrl
-)) as {
- default: StartRequestHandler
-}
-
-function buildRedirectRequest(random: () => number) {
- return new Request(
- `http://localhost/from/${randomSegment(random)}`,
- requestInit,
- )
-}
-
-function buildNotFoundRequest(random: () => number) {
- return new Request(
- `http://localhost/missing/${randomSegment(random)}`,
- requestInit,
- )
-}
-
-function buildErrorRequest(random: () => number) {
- return new Request(
- `http://localhost/boom/${randomSegment(random)}`,
- requestInit,
- )
-}
-
-function buildUnmatchedRequest(random: () => number) {
- return new Request(
- `http://localhost/nope/${randomSegment(random)}`,
- requestInit,
- )
-}
-
-function getRequestId(request: Request) {
- const id = new URL(request.url).pathname.split('/').pop()
-
- if (!id) {
- throw new Error(`Expected request id in ${request.url}`)
- }
-
- return id
-}
-
-function validateRedirectResponse(response: Response, request: Request) {
- if (response.status !== redirectStatus) {
- throw new Error(
- `Expected status ${redirectStatus} for ${request.url}, got ${response.status}`,
- )
- }
-
- const id = getRequestId(request)
- const location = response.headers.get('location')
-
- if (location !== `/target/${id}`) {
- throw new Error(`Expected redirect location /target/${id}, got ${location}`)
- }
-}
-
-function validateNotFoundResponse(response: Response, request: Request) {
- if (response.status !== notFoundStatus) {
- throw new Error(
- `Expected status ${notFoundStatus} for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validateErrorResponse(response: Response, request: Request) {
- if (response.status !== errorStatus) {
- throw new Error(
- `Expected status ${errorStatus} for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validateNotFoundBody(body: string) {
- if (!body.includes(notFoundMarker)) {
- throw new Error('Expected error-paths not-found marker in response body')
- }
-}
-
-function validateErrorBody(body: string) {
- if (!body.includes(errorMarker)) {
- throw new Error('Expected error-paths error marker in response body')
- }
-}
-
-async function assertStatusSanity(
- request: Request,
- validateResponse: (response: Response, request: Request) => void,
- validateBody?: (body: string) => void,
-) {
- const response = await handler.fetch(request)
- validateResponse(response, request)
-
- const body = await response.text()
- validateBody?.(body)
-}
-
-async function assertErrorPathsSanity() {
- await assertStatusSanity(
- new Request('http://localhost/from/sanity-redirect', requestInit),
- validateRedirectResponse,
- )
- await assertStatusSanity(
- new Request('http://localhost/missing/sanity-missing', requestInit),
- validateNotFoundResponse,
- validateNotFoundBody,
- )
- await assertStatusSanity(
- new Request('http://localhost/boom/sanity-error', requestInit),
- validateErrorResponse,
- validateErrorBody,
- )
- await assertStatusSanity(
- new Request('http://localhost/nope/sanity-unmatched', requestInit),
- validateNotFoundResponse,
- )
-}
-
-await assertErrorPathsSanity()
+const test = await setup()
+await test.sanity()
describe('memory', () => {
- bench(
- 'mem error-paths redirect (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: redirectSeed,
- iterations: errorPathsIterations,
- buildRequest: buildRedirectRequest,
- validateResponse: validateRedirectResponse,
- }),
- memoryBenchOptions,
- )
-
- bench(
- 'mem error-paths not-found (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: notFoundSeed,
- iterations: errorPathsIterations,
- buildRequest: buildNotFoundRequest,
- validateResponse: validateNotFoundResponse,
- }),
- memoryBenchOptions,
- )
-
- bench(
- 'mem error-paths error (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: errorSeed,
- iterations: errorPathsIterations,
- buildRequest: buildErrorRequest,
- validateResponse: validateErrorResponse,
- }),
- memoryBenchOptions,
- )
-
- bench(
- 'mem error-paths unmatched (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: unmatchedSeed,
- iterations: errorPathsIterations,
- buildRequest: buildUnmatchedRequest,
- validateResponse: validateNotFoundResponse,
- }),
- memoryBenchOptions,
- )
+ registerServerMemoryBenches(test)
})
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/project.json b/benchmarks/memory/server/scenarios/error-paths/react/project.json
index 710048160f..6a399fe41e 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/project.json
+++ b/benchmarks/memory/server/scenarios/error-paths/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:ssr": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
new file mode 100644
index 0000000000..d51bebd948
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
@@ -0,0 +1,212 @@
+import {
+ randomSegment,
+ runSequentialRequestLoop,
+} from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const errorPathsIterations = 50
+const redirectSeed = 0xdecafbad
+const notFoundSeed = 0xdecafb0d
+const errorSeed = 0xdecafbed
+const unmatchedSeed = 0xdecaf00d
+
+const redirectStatus = 302
+const notFoundStatus = 404
+const errorStatus = 500
+const notFoundMarker = 'data-bench="not-found-boundary"'
+const errorMarker = 'data-bench="error-boundary"'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildRedirectRequest(random: () => number) {
+ return new Request(
+ `http://localhost/from/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function buildNotFoundRequest(random: () => number) {
+ return new Request(
+ `http://localhost/missing/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function buildErrorRequest(random: () => number) {
+ return new Request(
+ `http://localhost/boom/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function buildUnmatchedRequest(random: () => number) {
+ return new Request(
+ `http://localhost/nope/${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function getRequestId(request: Request) {
+ const id = new URL(request.url).pathname.split('/').pop()
+
+ if (!id) {
+ throw new Error(`Expected request id in ${request.url}`)
+ }
+
+ return id
+}
+
+function validateRedirectResponse(response: Response, request: Request) {
+ if (response.status !== redirectStatus) {
+ throw new Error(
+ `Expected status ${redirectStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+
+ const id = getRequestId(request)
+ const location = response.headers.get('location')
+
+ if (location !== `/target/${id}`) {
+ throw new Error(`Expected redirect location /target/${id}, got ${location}`)
+ }
+}
+
+function validateNotFoundResponse(response: Response, request: Request) {
+ if (response.status !== notFoundStatus) {
+ throw new Error(
+ `Expected status ${notFoundStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateErrorResponse(response: Response, request: Request) {
+ if (response.status !== errorStatus) {
+ throw new Error(
+ `Expected status ${errorStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateNotFoundBody(body: string) {
+ if (!body.includes(notFoundMarker)) {
+ throw new Error('Expected error-paths not-found marker in response body')
+ }
+}
+
+function validateErrorBody(body: string) {
+ if (!body.includes(errorMarker)) {
+ throw new Error('Expected error-paths error marker in response body')
+ }
+}
+
+async function assertStatusSanity(
+ handler: StartRequestHandler,
+ request: Request,
+ validateResponse: (response: Response, request: Request) => void,
+ validateBody?: (body: string) => void,
+) {
+ const response = await handler.fetch(request)
+ validateResponse(response, request)
+
+ const body = await response.text()
+ validateBody?.(body)
+}
+
+async function assertErrorPathsSanity(handler: StartRequestHandler) {
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/from/sanity-redirect', requestInit),
+ validateRedirectResponse,
+ )
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/missing/sanity-missing', requestInit),
+ validateNotFoundResponse,
+ validateNotFoundBody,
+ )
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/boom/sanity-error', requestInit),
+ validateErrorResponse,
+ validateErrorBody,
+ )
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/nope/sanity-unmatched', requestInit),
+ validateNotFoundResponse,
+ )
+}
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ const runRedirect = () =>
+ runSequentialRequestLoop(handler, {
+ seed: redirectSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildRedirectRequest,
+ validateResponse: validateRedirectResponse,
+ })
+
+ const runNotFound = () =>
+ runSequentialRequestLoop(handler, {
+ seed: notFoundSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildNotFoundRequest,
+ validateResponse: validateNotFoundResponse,
+ })
+
+ const runError = () =>
+ runSequentialRequestLoop(handler, {
+ seed: errorSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildErrorRequest,
+ validateResponse: validateErrorResponse,
+ })
+
+ const runUnmatched = () =>
+ runSequentialRequestLoop(handler, {
+ seed: unmatchedSeed,
+ iterations: errorPathsIterations,
+ buildRequest: buildUnmatchedRequest,
+ validateResponse: validateNotFoundResponse,
+ })
+
+ return {
+ sanity: () => assertErrorPathsSanity(handler),
+ async run() {
+ await runRedirect()
+ await runNotFound()
+ await runError()
+ await runUnmatched()
+ },
+ benches: [
+ {
+ name: 'mem error-paths redirect (react)',
+ run: runRedirect,
+ },
+ {
+ name: 'mem error-paths not-found (react)',
+ run: runNotFound,
+ },
+ {
+ name: 'mem error-paths error (react)',
+ run: runError,
+ },
+ {
+ name: 'mem error-paths unmatched (react)',
+ run: runUnmatched,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json
index 4d78884cea..11ddcce4ea 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json
+++ b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json
@@ -1,12 +1,15 @@
{
"extends": "../../../../../../tsconfig.json",
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": [
"memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
"./src/**/*"
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
index cbe4449783..1dc2e0da86 100644
--- a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts
@@ -1,75 +1,10 @@
-import { bench, describe } from 'vitest'
-import {
- memoryBenchOptions,
- runSequentialRequestLoop,
-} from '../../../bench-utils'
-import type { StartRequestHandler } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
-const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const benchmarkSeed = 0x5eed_0005
-const peakLargePageIterations = 2
-const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8'
-const levelEightMarker = 'data-bench="peak-large-page-level-8"'
-const knownDehydratedRecordName = 'peak-large-page-l8-record-199'
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-const { default: handler } = (await import(
- /* @vite-ignore */ appModuleUrl
-)) as {
- default: StartRequestHandler
-}
-
-function buildPeakLargePageRequest() {
- return new Request(peakLargePageUrl, requestInit)
-}
-
-function validatePeakLargePageResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validatePeakLargePageBody(body: string) {
- if (!body.includes(levelEightMarker)) {
- throw new Error('Expected peak-large-page level-8 marker in response body')
- }
-
- if (!body.includes(knownDehydratedRecordName)) {
- throw new Error(
- 'Expected peak-large-page dehydrated record in response body',
- )
- }
-}
-
-async function assertPeakLargePageSanity(handler: StartRequestHandler) {
- const request = new Request(peakLargePageUrl, requestInit)
- const response = await handler.fetch(request)
- const body = await response.text()
-
- validatePeakLargePageResponse(response, request)
- validatePeakLargePageBody(body)
-}
-
-await assertPeakLargePageSanity(handler)
+const test = await setup()
+await test.sanity()
describe('memory', () => {
- bench(
- 'mem peak-large-page (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: benchmarkSeed,
- iterations: peakLargePageIterations,
- buildRequest: buildPeakLargePageRequest,
- validateResponse: validatePeakLargePageResponse,
- }),
- memoryBenchOptions,
- )
+ registerServerMemoryBenches(test)
})
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/project.json b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json
index 89a5865a74..699a243a03 100644
--- a/benchmarks/memory/server/scenarios/peak-large-page/react/project.json
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:ssr": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
new file mode 100644
index 0000000000..e10d034aa2
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
@@ -0,0 +1,76 @@
+import { runSequentialRequestLoop } from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0x5eed_0005
+const peakLargePageIterations = 2
+const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8'
+const levelEightMarker = 'data-bench="peak-large-page-level-8"'
+const knownDehydratedRecordName = 'peak-large-page-l8-record-199'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildPeakLargePageRequest() {
+ return new Request(peakLargePageUrl, requestInit)
+}
+
+function validatePeakLargePageResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validatePeakLargePageBody(body: string) {
+ if (!body.includes(levelEightMarker)) {
+ throw new Error('Expected peak-large-page level-8 marker in response body')
+ }
+
+ if (!body.includes(knownDehydratedRecordName)) {
+ throw new Error(
+ 'Expected peak-large-page dehydrated record in response body',
+ )
+ }
+}
+
+async function assertPeakLargePageSanity(handler: StartRequestHandler) {
+ const request = new Request(peakLargePageUrl, requestInit)
+ const response = await handler.fetch(request)
+ const body = await response.text()
+
+ validatePeakLargePageResponse(response, request)
+ validatePeakLargePageBody(body)
+}
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ const run = () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: peakLargePageIterations,
+ buildRequest: buildPeakLargePageRequest,
+ validateResponse: validatePeakLargePageResponse,
+ })
+
+ return {
+ sanity: () => assertPeakLargePageSanity(handler),
+ run,
+ benches: [
+ {
+ name: 'mem peak-large-page (react)',
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json
index 4d78884cea..11ddcce4ea 100644
--- a/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json
@@ -1,12 +1,15 @@
{
"extends": "../../../../../../tsconfig.json",
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": [
"memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
"./src/**/*"
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
index bad7ed132e..1dc2e0da86 100644
--- a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts
@@ -1,82 +1,10 @@
-import { bench, describe } from 'vitest'
-import {
- memoryBenchOptions,
- randomSegment,
- runSequentialRequestLoop,
-} from '../../../bench-utils'
-import type { StartRequestHandler } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
-const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const benchmarkSeed = 0xdecafbad
-const requestChurnIterations = 200
-const itemPageMarker = 'data-bench="request-churn-item"'
-const dehydrationMarker = '$_TSR'
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-const { default: handler } = (await import(
- /* @vite-ignore */ appModuleUrl
-)) as {
- default: StartRequestHandler
-}
-
-function buildItemRequest(random: () => number) {
- const id = randomSegment(random)
- const q = `q-${randomSegment(random)}`
-
- return new Request(`http://localhost/items/${id}?q=${q}`, requestInit)
-}
-
-function validateItemResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validateItemBody(body: string) {
- if (!body.includes(itemPageMarker)) {
- throw new Error('Expected request-churn item marker in response body')
- }
-}
-
-async function assertRequestChurnSanity(handler: StartRequestHandler) {
- const response = await handler.fetch(
- new Request('http://localhost/items/sanity-item?q=q-sanity', requestInit),
- )
- const body = await response.text()
-
- if (response.status !== 200) {
- throw new Error(`Expected sanity status 200, got ${response.status}`)
- }
-
- validateItemBody(body)
-
- if (!body.includes(dehydrationMarker)) {
- throw new Error(
- 'Expected sanity response to include the dehydration marker',
- )
- }
-}
-
-await assertRequestChurnSanity(handler)
+const test = await setup()
+await test.sanity()
describe('memory', () => {
- bench(
- 'mem request-churn (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: benchmarkSeed,
- iterations: requestChurnIterations,
- buildRequest: buildItemRequest,
- validateResponse: validateItemResponse,
- }),
- memoryBenchOptions,
- )
+ registerServerMemoryBenches(test)
})
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/project.json b/benchmarks/memory/server/scenarios/request-churn/react/project.json
index 03dce4042a..d997054b95 100644
--- a/benchmarks/memory/server/scenarios/request-churn/react/project.json
+++ b/benchmarks/memory/server/scenarios/request-churn/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:ssr": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
new file mode 100644
index 0000000000..733578ce7f
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
@@ -0,0 +1,85 @@
+import {
+ randomSegment,
+ runSequentialRequestLoop,
+} from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0xdecafbad
+const requestChurnIterations = 200
+const itemPageMarker = 'data-bench="request-churn-item"'
+const dehydrationMarker = '$_TSR'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildItemRequest(random: () => number) {
+ const id = randomSegment(random)
+ const q = `q-${randomSegment(random)}`
+
+ return new Request(`http://localhost/items/${id}?q=${q}`, requestInit)
+}
+
+function validateItemResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateItemBody(body: string) {
+ if (!body.includes(itemPageMarker)) {
+ throw new Error('Expected request-churn item marker in response body')
+ }
+}
+
+async function assertRequestChurnSanity(handler: StartRequestHandler) {
+ const response = await handler.fetch(
+ new Request('http://localhost/items/sanity-item?q=q-sanity', requestInit),
+ )
+ const body = await response.text()
+
+ if (response.status !== 200) {
+ throw new Error(`Expected sanity status 200, got ${response.status}`)
+ }
+
+ validateItemBody(body)
+
+ if (!body.includes(dehydrationMarker)) {
+ throw new Error(
+ 'Expected sanity response to include the dehydration marker',
+ )
+ }
+}
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ const run = () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: requestChurnIterations,
+ buildRequest: buildItemRequest,
+ validateResponse: validateItemResponse,
+ })
+
+ return {
+ sanity: () => assertRequestChurnSanity(handler),
+ run,
+ benches: [
+ {
+ name: 'mem request-churn (react)',
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json
index 4d78884cea..11ddcce4ea 100644
--- a/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json
+++ b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json
@@ -1,12 +1,15 @@
{
"extends": "../../../../../../tsconfig.json",
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": [
"memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
"./src/**/*"
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
index 95f409aa11..1dc2e0da86 100644
--- a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts
@@ -1,104 +1,10 @@
-import { bench, describe } from 'vitest'
-import {
- memoryBenchOptions,
- randomSegment,
- runSequentialRequestLoop,
-} from '../../../bench-utils'
-import type { StartRequestHandler } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
-const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const benchmarkSeed = 0x51eaa11
-const serializationPayloadIterations = 5
-const payloadPageMarker = 'data-bench="serialization-payload"'
-const dehydrationMarker = '$_TSR'
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-const { default: handler } = (await import(
- /* @vite-ignore */ appModuleUrl
-)) as {
- default: StartRequestHandler
-}
-
-function buildPayloadRequest(random: () => number, index: number) {
- const id = `payload-${index}-${randomSegment(random)}`
-
- return new Request(`http://localhost/data/${id}`, requestInit)
-}
-
-function knownMapKey(id: string) {
- return `map-${id}-000`
-}
-
-function getRequestId(request: Request) {
- const url = new URL(request.url)
- const match = /^\/data\/([^/]+)$/.exec(url.pathname)
- const id = match?.[1]
-
- if (id === undefined) {
- throw new Error(`Expected /data/$id request URL, got ${request.url}`)
- }
-
- return decodeURIComponent(id)
-}
-
-function validatePayloadResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validatePayloadBody(
- body: string,
- _response: Response,
- request: Request,
-) {
- if (!body.includes(payloadPageMarker)) {
- throw new Error('Expected serialization-payload marker in response body')
- }
-
- if (!body.includes(dehydrationMarker)) {
- throw new Error('Expected serialization-payload dehydration script in body')
- }
-
- const mapKey = knownMapKey(getRequestId(request))
-
- if (!body.includes(mapKey)) {
- throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`)
- }
-}
-
-async function assertSerializationPayloadSanity(handler: StartRequestHandler) {
- const request = new Request(
- 'http://localhost/data/sanity-payload',
- requestInit,
- )
- const response = await handler.fetch(request)
- const body = await response.text()
-
- validatePayloadResponse(response, request)
- validatePayloadBody(body, response, request)
-}
-
-await assertSerializationPayloadSanity(handler)
+const test = await setup()
+await test.sanity()
describe('memory', () => {
- bench(
- 'mem serialization-payload (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: benchmarkSeed,
- iterations: serializationPayloadIterations,
- buildRequest: buildPayloadRequest,
- validateResponse: validatePayloadResponse,
- }),
- memoryBenchOptions,
- )
+ registerServerMemoryBenches(test)
})
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/project.json b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json
index 5cc35a789f..467b40d734 100644
--- a/benchmarks/memory/server/scenarios/serialization-payload/react/project.json
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:ssr": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
new file mode 100644
index 0000000000..f9242dd942
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
@@ -0,0 +1,107 @@
+import {
+ randomSegment,
+ runSequentialRequestLoop,
+} from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0x51eaa11
+const serializationPayloadIterations = 5
+const payloadPageMarker = 'data-bench="serialization-payload"'
+const dehydrationMarker = '$_TSR'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildPayloadRequest(random: () => number, index: number) {
+ const id = `payload-${index}-${randomSegment(random)}`
+
+ return new Request(`http://localhost/data/${id}`, requestInit)
+}
+
+function knownMapKey(id: string) {
+ return `map-${id}-000`
+}
+
+function getRequestId(request: Request) {
+ const url = new URL(request.url)
+ const match = /^\/data\/([^/]+)$/.exec(url.pathname)
+ const id = match?.[1]
+
+ if (id === undefined) {
+ throw new Error(`Expected /data/$id request URL, got ${request.url}`)
+ }
+
+ return decodeURIComponent(id)
+}
+
+function validatePayloadResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validatePayloadBody(
+ body: string,
+ _response: Response,
+ request: Request,
+) {
+ if (!body.includes(payloadPageMarker)) {
+ throw new Error('Expected serialization-payload marker in response body')
+ }
+
+ if (!body.includes(dehydrationMarker)) {
+ throw new Error('Expected serialization-payload dehydration script in body')
+ }
+
+ const mapKey = knownMapKey(getRequestId(request))
+
+ if (!body.includes(mapKey)) {
+ throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`)
+ }
+}
+
+async function assertSerializationPayloadSanity(handler: StartRequestHandler) {
+ const request = new Request(
+ 'http://localhost/data/sanity-payload',
+ requestInit,
+ )
+ const response = await handler.fetch(request)
+ const body = await response.text()
+
+ validatePayloadResponse(response, request)
+ validatePayloadBody(body, response, request)
+}
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ const run = () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: serializationPayloadIterations,
+ buildRequest: buildPayloadRequest,
+ validateResponse: validatePayloadResponse,
+ })
+
+ return {
+ sanity: () => assertSerializationPayloadSanity(handler),
+ run,
+ benches: [
+ {
+ name: 'mem serialization-payload (react)',
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json
index 4d78884cea..11ddcce4ea 100644
--- a/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json
@@ -1,12 +1,15 @@
{
"extends": "../../../../../../tsconfig.json",
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": [
"memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
"./src/**/*"
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
index 4945ebdaf9..1dc2e0da86 100644
--- a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts
@@ -1,237 +1,10 @@
-import { bench, describe } from 'vitest'
-import {
- createDeterministicRandom,
- memoryBenchOptions,
- randomSegment,
- runSequentialRequestLoop,
-} from '../../../bench-utils'
-import type { StartRequestHandler } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
-type FnUrls = {
- get: string
- post: string
-}
-
-type PayloadFixture = {
- id: string
- body: string
- query: string
-}
-
-type SerovalNode =
- | {
- t: 1
- s: string
- }
- | {
- t: 10
- i: number
- p: {
- k: Array
- v: Array
- }
- o: number
- }
-
-const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const benchmarkSeed = 0xdecafbad
-const payloadSeed = 0x51f0cafe
-const fixtureCount = 16
-const serverFnChurnIterations = 150
-const origin = 'http://localhost'
-const tssContentTypeFramed = 'application/x-tss-framed'
-const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json`
-const xTssSerialized = 'x-tss-serialized'
-const contextMarker = 'ctx-server-fn-churn'
-
-const commonHeaders = {
- 'x-tsr-serverFn': 'true',
- 'sec-fetch-site': 'same-origin',
- accept: acceptHeader,
-} satisfies HeadersInit
-
-const postHeaders = {
- ...commonHeaders,
- 'content-type': 'application/json',
-} satisfies HeadersInit
-
-// Hand-rolled copy of Start's seroval RPC wire format so POST bodies can be
-// precomputed at module level. Coupled to the internal protocol on purpose;
-// the module-load sanity check below throws loudly if the protocol drifts.
-function stringNode(value: string): SerovalNode {
- return { t: 1, s: value }
-}
-
-function objectNode(
- id: number,
- entries: Array,
-): SerovalNode {
- return {
- t: 10,
- i: id,
- p: {
- k: entries.map(([key]) => key),
- v: entries.map(([, value]) => value),
- },
- o: 0,
- }
-}
-
-function serializePayload(id: string) {
- return JSON.stringify({
- t: objectNode(0, [['data', objectNode(1, [['id', stringNode(id)]])]]),
- f: 63,
- m: [],
- })
-}
-
-function createFixtures(kind: 'get' | 'post') {
- const random = createDeterministicRandom(payloadSeed ^ kind.length)
-
- return Array.from({ length: fixtureCount }, (_, index): PayloadFixture => {
- const id = [kind, index, randomSegment(random), randomSegment(random)].join(
- '-',
- )
- const body = serializePayload(id)
-
- return {
- id,
- body,
- query: `?${new URLSearchParams({ payload: body })}`,
- }
- })
-}
-
-const getFixtures = createFixtures('get')
-const postFixtures = createFixtures('post')
-
-const { default: handler } = (await import(
- /* @vite-ignore */ appModuleUrl
-)) as {
- default: StartRequestHandler
-}
-
-async function discoverUrls(handler: StartRequestHandler) {
- const response = await handler.fetch(new Request(`${origin}/api/fn-urls`))
- const text = await response.text()
-
- if (response.status !== 200) {
- throw new Error(
- `URL discovery failed with status ${response.status}: ${text}`,
- )
- }
-
- let urls: Partial
-
- try {
- urls = JSON.parse(text) as Partial
- } catch (error) {
- throw new Error(`URL discovery returned invalid JSON: ${text}`, {
- cause: error,
- })
- }
-
- if (typeof urls.get !== 'string' || typeof urls.post !== 'string') {
- throw new Error(`URL discovery returned invalid payload: ${text}`)
- }
-
- return urls as FnUrls
-}
-
-function buildGetRequest(url: string, fixture: PayloadFixture) {
- return new Request(`${origin}${url}${fixture.query}`, {
- method: 'GET',
- headers: commonHeaders,
- })
-}
-
-function buildPostRequest(url: string, fixture: PayloadFixture) {
- return new Request(`${origin}${url}`, {
- method: 'POST',
- headers: postHeaders,
- body: fixture.body,
- })
-}
-
-function validateServerFnResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-
- if (!response.headers.get(xTssSerialized)) {
- throw new Error(`Expected ${xTssSerialized} header for ${request.url}`)
- }
-}
-
-function validateEchoedBody(
- body: string,
- request: Request,
- expectedId: string,
-) {
- if (!body.includes(expectedId)) {
- throw new Error(`Expected echoed id ${expectedId} in ${request.url}`)
- }
-
- if (!body.includes(contextMarker)) {
- throw new Error(
- `Expected context marker ${contextMarker} in ${request.url}`,
- )
- }
-
- if (!body.includes(`${expectedId}-4`)) {
- throw new Error(`Expected final payload record for ${expectedId}`)
- }
-}
-
-async function assertServerFnChurnSanity(
- handler: StartRequestHandler,
- urls: FnUrls,
-) {
- const getFixture = getFixtures[0]!
- const getRequest = buildGetRequest(urls.get, getFixture)
- const getResponse = await handler.fetch(getRequest)
- const getBody = await getResponse.text()
-
- validateServerFnResponse(getResponse, getRequest)
- validateEchoedBody(getBody, getRequest, getFixture.id)
-
- const postFixture = postFixtures[0]!
- const postRequest = buildPostRequest(urls.post, postFixture)
- const postResponse = await handler.fetch(postRequest)
- const postBody = await postResponse.text()
-
- validateServerFnResponse(postResponse, postRequest)
- validateEchoedBody(postBody, postRequest, postFixture.id)
-}
-
-const urls = await discoverUrls(handler)
-
-await assertServerFnChurnSanity(handler, urls)
+const test = await setup()
+await test.sanity()
describe('memory', () => {
- bench(
- 'mem server-fn-churn (react)',
- () =>
- runSequentialRequestLoop(handler, {
- seed: benchmarkSeed,
- iterations: serverFnChurnIterations,
- buildRequest: (_random, index) => {
- const fixtureIndex = Math.floor(index / 2) % fixtureCount
-
- if (index % 2 === 0) {
- const fixture = getFixtures[fixtureIndex]!
-
- return buildGetRequest(urls.get, fixture)
- }
-
- const fixture = postFixtures[fixtureIndex]!
-
- return buildPostRequest(urls.post, fixture)
- },
- validateResponse: validateServerFnResponse,
- }),
- memoryBenchOptions,
- )
+ registerServerMemoryBenches(test)
})
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json
index 926a50b9f1..841bbedca0 100644
--- a/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:ssr": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
new file mode 100644
index 0000000000..c93fc4ab9b
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
@@ -0,0 +1,239 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+ runSequentialRequestLoop,
+} from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+type FnUrls = {
+ get: string
+ post: string
+}
+
+type PayloadFixture = {
+ id: string
+ body: string
+ query: string
+}
+
+type SerovalNode =
+ | {
+ t: 1
+ s: string
+ }
+ | {
+ t: 10
+ i: number
+ p: {
+ k: Array
+ v: Array
+ }
+ o: number
+ }
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0xdecafbad
+const payloadSeed = 0x51f0cafe
+const fixtureCount = 16
+const serverFnChurnIterations = 150
+const origin = 'http://localhost'
+const tssContentTypeFramed = 'application/x-tss-framed'
+const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json`
+const xTssSerialized = 'x-tss-serialized'
+const contextMarker = 'ctx-server-fn-churn'
+
+const commonHeaders = {
+ 'x-tsr-serverFn': 'true',
+ 'sec-fetch-site': 'same-origin',
+ accept: acceptHeader,
+} satisfies HeadersInit
+
+const postHeaders = {
+ ...commonHeaders,
+ 'content-type': 'application/json',
+} satisfies HeadersInit
+
+// Hand-rolled copy of Start's seroval RPC wire format so POST bodies can be
+// precomputed at module level. Coupled to the internal protocol on purpose;
+// the module-load sanity check below throws loudly if the protocol drifts.
+function stringNode(value: string): SerovalNode {
+ return { t: 1, s: value }
+}
+
+function objectNode(
+ id: number,
+ entries: Array,
+): SerovalNode {
+ return {
+ t: 10,
+ i: id,
+ p: {
+ k: entries.map(([key]) => key),
+ v: entries.map(([, value]) => value),
+ },
+ o: 0,
+ }
+}
+
+function serializePayload(id: string) {
+ return JSON.stringify({
+ t: objectNode(0, [['data', objectNode(1, [['id', stringNode(id)]])]]),
+ f: 63,
+ m: [],
+ })
+}
+
+function createFixtures(kind: 'get' | 'post') {
+ const random = createDeterministicRandom(payloadSeed ^ kind.length)
+
+ return Array.from({ length: fixtureCount }, (_, index): PayloadFixture => {
+ const id = [kind, index, randomSegment(random), randomSegment(random)].join(
+ '-',
+ )
+ const body = serializePayload(id)
+
+ return {
+ id,
+ body,
+ query: `?${new URLSearchParams({ payload: body })}`,
+ }
+ })
+}
+
+const getFixtures = createFixtures('get')
+const postFixtures = createFixtures('post')
+
+async function discoverUrls(handler: StartRequestHandler) {
+ const response = await handler.fetch(new Request(`${origin}/api/fn-urls`))
+ const text = await response.text()
+
+ if (response.status !== 200) {
+ throw new Error(
+ `URL discovery failed with status ${response.status}: ${text}`,
+ )
+ }
+
+ let urls: Partial
+
+ try {
+ urls = JSON.parse(text) as Partial
+ } catch (error) {
+ throw new Error(`URL discovery returned invalid JSON: ${text}`, {
+ cause: error,
+ })
+ }
+
+ if (typeof urls.get !== 'string' || typeof urls.post !== 'string') {
+ throw new Error(`URL discovery returned invalid payload: ${text}`)
+ }
+
+ return urls as FnUrls
+}
+
+function buildGetRequest(url: string, fixture: PayloadFixture) {
+ return new Request(`${origin}${url}${fixture.query}`, {
+ method: 'GET',
+ headers: commonHeaders,
+ })
+}
+
+function buildPostRequest(url: string, fixture: PayloadFixture) {
+ return new Request(`${origin}${url}`, {
+ method: 'POST',
+ headers: postHeaders,
+ body: fixture.body,
+ })
+}
+
+function validateServerFnResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+
+ if (!response.headers.get(xTssSerialized)) {
+ throw new Error(`Expected ${xTssSerialized} header for ${request.url}`)
+ }
+}
+
+function validateEchoedBody(
+ body: string,
+ request: Request,
+ expectedId: string,
+) {
+ if (!body.includes(expectedId)) {
+ throw new Error(`Expected echoed id ${expectedId} in ${request.url}`)
+ }
+
+ if (!body.includes(contextMarker)) {
+ throw new Error(
+ `Expected context marker ${contextMarker} in ${request.url}`,
+ )
+ }
+
+ if (!body.includes(`${expectedId}-4`)) {
+ throw new Error(`Expected final payload record for ${expectedId}`)
+ }
+}
+
+async function assertServerFnChurnSanity(
+ handler: StartRequestHandler,
+ urls: FnUrls,
+) {
+ const getFixture = getFixtures[0]!
+ const getRequest = buildGetRequest(urls.get, getFixture)
+ const getResponse = await handler.fetch(getRequest)
+ const getBody = await getResponse.text()
+
+ validateServerFnResponse(getResponse, getRequest)
+ validateEchoedBody(getBody, getRequest, getFixture.id)
+
+ const postFixture = postFixtures[0]!
+ const postRequest = buildPostRequest(urls.post, postFixture)
+ const postResponse = await handler.fetch(postRequest)
+ const postBody = await postResponse.text()
+
+ validateServerFnResponse(postResponse, postRequest)
+ validateEchoedBody(postBody, postRequest, postFixture.id)
+}
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+ const urls = await discoverUrls(handler)
+
+ const run = () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: serverFnChurnIterations,
+ buildRequest: (_random, index) => {
+ const fixtureIndex = Math.floor(index / 2) % fixtureCount
+
+ if (index % 2 === 0) {
+ const fixture = getFixtures[fixtureIndex]!
+
+ return buildGetRequest(urls.get, fixture)
+ }
+
+ const fixture = postFixtures[fixtureIndex]!
+
+ return buildPostRequest(urls.post, fixture)
+ },
+ validateResponse: validateServerFnResponse,
+ })
+
+ return {
+ sanity: () => assertServerFnChurnSanity(handler, urls),
+ run,
+ benches: [
+ {
+ name: 'mem server-fn-churn (react)',
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json
index 4d78884cea..11ddcce4ea 100644
--- a/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json
@@ -1,12 +1,15 @@
{
"extends": "../../../../../../tsconfig.json",
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": [
"memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
"./src/**/*"
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
index cf0e2fa5ea..1dc2e0da86 100644
--- a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts
@@ -1,145 +1,10 @@
-import { bench, describe } from 'vitest'
-import {
- memoryBenchOptions,
- randomSegment,
- runSequentialRequestLoop,
-} from '../../../bench-utils'
-import type { StartRequestHandler } from '../../../bench-utils'
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
-const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const benchmarkSeed = 0xdecafbad
-const streamingPeakIterations = 3
-const fallbackMarkers = [
- 'streaming-peak-fallback-0',
- 'streaming-peak-fallback-1',
- 'streaming-peak-fallback-2',
- 'streaming-peak-fallback-3',
-] as const
-const deferredSectionMarkers = [
- 'streaming-peak-deferred-0',
- 'streaming-peak-deferred-1',
- 'streaming-peak-deferred-2',
- 'streaming-peak-deferred-3',
-] as const
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-const { default: handler } = (await import(
- /* @vite-ignore */ appModuleUrl
-)) as {
- default: StartRequestHandler
-}
-
-function buildStreamingRequest(random: () => number, index: number) {
- return new Request(
- `http://localhost/stream/${index}-${randomSegment(random)}`,
- requestInit,
- )
-}
-
-function validateStreamingResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function getResponseReader(response: Response) {
- const reader = response.body?.getReader()
-
- if (!reader) {
- throw new Error('Expected streaming response body')
- }
-
- return reader
-}
-
-async function readStreamingBody(response: Response) {
- const reader = getResponseReader(response)
- const decoder = new TextDecoder()
- let body = ''
- let chunkCount = 0
-
- while (true) {
- const result = await reader.read()
-
- if (result.done) {
- break
- }
-
- chunkCount++
- body += decoder.decode(result.value, { stream: true })
- }
-
- body += decoder.decode()
-
- return { body, chunkCount }
-}
-
-function assertFallbacksPrecedeDeferredContent(body: string) {
- for (let index = 0; index < fallbackMarkers.length; index++) {
- const fallbackIndex = body.indexOf(fallbackMarkers[index]!)
- const deferredIndex = body.indexOf(deferredSectionMarkers[index]!)
-
- if (fallbackIndex === -1) {
- throw new Error(
- `Expected fallback marker ${fallbackMarkers[index]} in body`,
- )
- }
-
- if (deferredIndex === -1) {
- throw new Error(
- `Expected deferred section marker ${deferredSectionMarkers[index]} in body`,
- )
- }
-
- if (fallbackIndex > deferredIndex) {
- throw new Error(
- `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`,
- )
- }
- }
-}
-
-async function assertStreamingPeakSanity(handler: StartRequestHandler) {
- const chunkedRequest = new Request(
- 'http://localhost/stream/sanity-chunked',
- requestInit,
- )
- const chunkedResponse = await handler.fetch(chunkedRequest)
-
- validateStreamingResponse(chunkedResponse, chunkedRequest)
-
- const chunked = await readStreamingBody(chunkedResponse)
-
- if (chunked.chunkCount <= 1) {
- throw new Error(
- `Expected chunked sanity response to produce multiple chunks, got ${chunked.chunkCount}`,
- )
- }
-
- assertFallbacksPrecedeDeferredContent(chunked.body)
-}
-
-await assertStreamingPeakSanity(handler)
+const test = await setup()
+await test.sanity()
describe('memory', () => {
- bench(
- 'mem streaming-peak chunked (react)',
- async () => {
- await runSequentialRequestLoop(handler, {
- seed: benchmarkSeed,
- iterations: streamingPeakIterations,
- buildRequest: buildStreamingRequest,
- validateResponse: validateStreamingResponse,
- })
- },
- memoryBenchOptions,
- )
+ registerServerMemoryBenches(test)
})
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/project.json b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json
index d6fb362055..6fc1dcdda0 100644
--- a/benchmarks/memory/server/scenarios/streaming-peak/react/project.json
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json
@@ -15,6 +15,29 @@
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/react-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
"test:types:ssr": {
"executor": "nx:run-commands",
"dependsOn": [
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
new file mode 100644
index 0000000000..75657e5998
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
@@ -0,0 +1,147 @@
+import {
+ randomSegment,
+ runSequentialRequestLoop,
+} from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+const benchmarkSeed = 0xdecafbad
+const streamingPeakIterations = 3
+const fallbackMarkers = [
+ 'streaming-peak-fallback-0',
+ 'streaming-peak-fallback-1',
+ 'streaming-peak-fallback-2',
+ 'streaming-peak-fallback-3',
+] as const
+const deferredSectionMarkers = [
+ 'streaming-peak-deferred-0',
+ 'streaming-peak-deferred-1',
+ 'streaming-peak-deferred-2',
+ 'streaming-peak-deferred-3',
+] as const
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildStreamingRequest(random: () => number, index: number) {
+ return new Request(
+ `http://localhost/stream/${index}-${randomSegment(random)}`,
+ requestInit,
+ )
+}
+
+function validateStreamingResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function getResponseReader(response: Response) {
+ const reader = response.body?.getReader()
+
+ if (!reader) {
+ throw new Error('Expected streaming response body')
+ }
+
+ return reader
+}
+
+async function readStreamingBody(response: Response) {
+ const reader = getResponseReader(response)
+ const decoder = new TextDecoder()
+ let body = ''
+ let chunkCount = 0
+
+ while (true) {
+ const result = await reader.read()
+
+ if (result.done) {
+ break
+ }
+
+ chunkCount++
+ body += decoder.decode(result.value, { stream: true })
+ }
+
+ body += decoder.decode()
+
+ return { body, chunkCount }
+}
+
+function assertFallbacksPrecedeDeferredContent(body: string) {
+ for (let index = 0; index < fallbackMarkers.length; index++) {
+ const fallbackIndex = body.indexOf(fallbackMarkers[index]!)
+ const deferredIndex = body.indexOf(deferredSectionMarkers[index]!)
+
+ if (fallbackIndex === -1) {
+ throw new Error(
+ `Expected fallback marker ${fallbackMarkers[index]} in body`,
+ )
+ }
+
+ if (deferredIndex === -1) {
+ throw new Error(
+ `Expected deferred section marker ${deferredSectionMarkers[index]} in body`,
+ )
+ }
+
+ if (fallbackIndex > deferredIndex) {
+ throw new Error(
+ `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`,
+ )
+ }
+ }
+}
+
+async function assertStreamingPeakSanity(handler: StartRequestHandler) {
+ const chunkedRequest = new Request(
+ 'http://localhost/stream/sanity-chunked',
+ requestInit,
+ )
+ const chunkedResponse = await handler.fetch(chunkedRequest)
+
+ validateStreamingResponse(chunkedResponse, chunkedRequest)
+
+ const chunked = await readStreamingBody(chunkedResponse)
+
+ if (chunked.chunkCount <= 1) {
+ throw new Error(
+ `Expected chunked sanity response to produce multiple chunks, got ${chunked.chunkCount}`,
+ )
+ }
+
+ assertFallbacksPrecedeDeferredContent(chunked.body)
+}
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ const run = () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: streamingPeakIterations,
+ buildRequest: buildStreamingRequest,
+ validateResponse: validateStreamingResponse,
+ })
+
+ return {
+ sanity: () => assertStreamingPeakSanity(handler),
+ run,
+ benches: [
+ {
+ name: 'mem streaming-peak chunked (react)',
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json
index 4d78884cea..11ddcce4ea 100644
--- a/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json
@@ -1,12 +1,15 @@
{
"extends": "../../../../../../tsconfig.json",
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": [
"memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
"vite.config.ts",
"../../../bench-utils.ts",
"./src/**/*"
diff --git a/benchmarks/memory/server/vitest.react.config.ts b/benchmarks/memory/server/vitest.react.config.ts
index 356ab7b3a8..44643b4d60 100644
--- a/benchmarks/memory/server/vitest.react.config.ts
+++ b/benchmarks/memory/server/vitest.react.config.ts
@@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
watch: false,
+ fileParallelism: false,
projects: ['./scenarios/*/react/vite.config.ts'],
},
})
diff --git a/package.json b/package.json
index 75bc153169..4868afde6a 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,8 @@
"benchmark:bundle-size:analyze": "node scripts/benchmarks/bundle-size/analyze.mjs",
"benchmark:client-nav": "nx run @benchmarks/client-nav:test:perf",
"benchmark:ssr": "nx run @benchmarks/ssr:test:perf",
+ "benchmark:memory:client:flame": "nx run @benchmarks/memory-client:test:flame:react --parallel=1",
+ "benchmark:memory:server:flame": "nx run @benchmarks/memory-server:test:flame:react --parallel=1",
"build": "nx affected --target=build --exclude=e2e/** --exclude=examples/**",
"build:all": "nx run-many --target=build --exclude=examples/** --exclude=e2e/**",
"watch": "pnpm run build:all && nx watch --all -- pnpm run build:all",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b30d7b42a1..dde49c342a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -311,7 +311,7 @@ importers:
version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
vitest:
specifier: ^4.1.4
- version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
benchmarks/memory/client:
dependencies:
@@ -331,6 +331,9 @@ importers:
'@codspeed/vitest-plugin':
specifier: ^5.5.0
version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)
+ '@platformatic/flame':
+ specifier: ^1.6.0
+ version: 1.6.0
'@testing-library/react':
specifier: ^16.2.0
version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -340,6 +343,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^6.0.1
version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ jsdom:
+ specifier: 29.1.1
+ version: 29.1.1(@noble/hashes@2.0.1)
typescript:
specifier: ^6.0.2
version: 6.0.2
@@ -348,7 +354,7 @@ importers:
version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)
vitest:
specifier: ^4.1.4
- version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
benchmarks/memory/server:
dependencies:
@@ -368,6 +374,9 @@ importers:
'@codspeed/vitest-plugin':
specifier: ^5.5.0
version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)
+ '@platformatic/flame':
+ specifier: ^1.6.0
+ version: 1.6.0
'@vitejs/plugin-react':
specifier: ^6.0.1
version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
@@ -379,7 +388,7 @@ importers:
version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)
vitest:
specifier: ^4.1.4
- version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
benchmarks/ssr:
dependencies:
@@ -437,7 +446,7 @@ importers:
version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
vitest:
specifier: ^4.1.4
- version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
e2e/e2e-utils:
devDependencies:
@@ -482,7 +491,7 @@ importers:
version: 5.9.2
vitest:
specifier: ^4.1.4
- version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
e2e/react-router/basepath-file-based:
dependencies:
@@ -1927,7 +1936,7 @@ importers:
version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
nitro:
specifier: ^3.0.260311-beta
- version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
sass:
specifier: ^1.97.2
version: 1.97.2
@@ -2092,7 +2101,7 @@ importers:
version: 2.0.1
'@rsbuild/plugin-react':
specifier: ^2.0.0
- version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21))
+ version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23))
'@tanstack/router-e2e-utils':
specifier: workspace:^
version: link:../../e2e-utils
@@ -2156,7 +2165,7 @@ importers:
version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
nitro:
specifier: ^3.0.260311-beta
- version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
srvx:
specifier: ^0.11.9
version: 0.11.12
@@ -2254,7 +2263,7 @@ importers:
version: 9.2.1
nitro:
specifier: ^3.0.260311-beta
- version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
typescript:
specifier: ^6.0.2
version: 6.0.2
@@ -3290,7 +3299,7 @@ importers:
version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
nitro:
specifier: ^3.0.260311-beta
- version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
typescript:
specifier: ^6.0.2
version: 6.0.2
@@ -3311,7 +3320,7 @@ importers:
version: link:../../../packages/start-static-server-functions
nitro:
specifier: ^3.0.1-alpha.2
- version: 3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
react:
specifier: ^19.2.3
version: 19.2.3
@@ -9005,7 +9014,7 @@ importers:
version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
nitro:
specifier: ^3.0.260311-beta
- version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
@@ -9747,7 +9756,7 @@ importers:
version: 0.5.20(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
nitro:
specifier: npm:nitro-nightly@latest
- version: nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
tailwindcss:
specifier: ^4.1.18
version: 4.2.2
@@ -11496,7 +11505,7 @@ importers:
version: 25.0.9
nitro:
specifier: ^3.0.260311-beta
- version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
@@ -11708,7 +11717,7 @@ importers:
version: 25.0.9
nitro:
specifier: ^3.0.260311-beta
- version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
@@ -13668,9 +13677,21 @@ packages:
'@asamuzakjp/css-color@4.0.5':
resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==}
+ '@asamuzakjp/css-color@5.1.11':
+ resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
'@asamuzakjp/dom-selector@6.5.6':
resolution: {integrity: sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==}
+ '@asamuzakjp/dom-selector@7.1.1':
+ resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/generational-cache@1.0.1':
+ resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
@@ -14008,6 +14029,10 @@ packages:
'@braidai/lang@1.1.2':
resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==}
+ '@bramus/specificity@2.4.2':
+ resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
+ hasBin: true
+
'@bundled-es-modules/cookie@2.0.1':
resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==}
@@ -14259,6 +14284,10 @@ packages:
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
+ '@csstools/color-helpers@6.0.2':
+ resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
+ engines: {node: '>=20.19.0'}
+
'@csstools/css-calc@2.1.1':
resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==}
engines: {node: '>=18'}
@@ -14273,6 +14302,13 @@ packages:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
+ '@csstools/css-calc@3.2.1':
+ resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
'@csstools/css-color-parser@3.0.7':
resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==}
engines: {node: '>=18'}
@@ -14287,6 +14323,13 @@ packages:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
+ '@csstools/css-color-parser@4.1.3':
+ resolution: {integrity: sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
'@csstools/css-parser-algorithms@3.0.4':
resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==}
engines: {node: '>=18'}
@@ -14299,12 +14342,26 @@ packages:
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
+ '@csstools/css-parser-algorithms@4.0.0':
+ resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^4.0.0
+
'@csstools/css-syntax-patches-for-csstree@1.0.14':
resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
+ '@csstools/css-syntax-patches-for-csstree@1.1.5':
+ resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==}
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+
'@csstools/css-tokenizer@3.0.3':
resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
engines: {node: '>=18'}
@@ -14313,6 +14370,10 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
+ '@csstools/css-tokenizer@4.0.0':
+ resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
+ engines: {node: '>=20.19.0'}
+
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
@@ -15284,6 +15345,15 @@ packages:
resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@exodus/bytes@1.15.1':
+ resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ '@noble/hashes': ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@noble/hashes':
+ optional: true
+
'@fastify/accept-negotiator@2.0.1':
resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==}
@@ -21064,6 +21134,10 @@ packages:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@@ -21109,6 +21183,10 @@ packages:
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
engines: {node: '>=20'}
+ data-urls@7.0.0:
+ resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
dataloader@1.4.0:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
@@ -21165,6 +21243,9 @@ packages:
decimal.js@10.5.0:
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
dedent@1.5.1:
resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==}
peerDependencies:
@@ -21507,6 +21588,10 @@ packages:
resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==}
engines: {node: '>=0.12'}
+ entities@8.0.0:
+ resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
+ engines: {node: '>=20.19.0'}
+
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -22418,6 +22503,10 @@ packages:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
+ html-encoding-sniffer@6.0.0:
+ resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
html-entities@2.3.3:
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
@@ -22960,6 +23049,15 @@ packages:
canvas:
optional: true
+ jsdom@29.1.1:
+ resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -23284,6 +23382,10 @@ packages:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22}
+ lru-cache@11.5.1:
+ resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -23353,6 +23455,9 @@ packages:
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -24016,6 +24121,9 @@ packages:
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ parse5@8.0.1:
+ resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
+
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -25491,6 +25599,10 @@ packages:
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
engines: {node: '>=16'}
+ tough-cookie@6.0.1:
+ resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
+ engines: {node: '>=16'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@@ -25702,6 +25814,10 @@ packages:
resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==}
engines: {node: '>=20.18.1'}
+ undici@7.27.2:
+ resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
+ engines: {node: '>=20.18.1'}
+
unenv@2.0.0-rc.24:
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
@@ -26386,6 +26502,10 @@ packages:
resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==}
engines: {node: '>=20'}
+ webidl-conversions@8.0.1:
+ resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
+ engines: {node: '>=20'}
+
webpack-cli@5.1.4:
resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==}
engines: {node: '>=14.15.0'}
@@ -26480,6 +26600,10 @@ packages:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
+ whatwg-mimetype@5.0.0:
+ resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
+ engines: {node: '>=20'}
+
whatwg-url@14.1.0:
resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==}
engines: {node: '>=18'}
@@ -26488,6 +26612,10 @@ packages:
resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
engines: {node: '>=20'}
+ whatwg-url@16.0.1:
+ resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -26769,6 +26897,14 @@ snapshots:
'@csstools/css-tokenizer': 3.0.4
lru-cache: 11.2.2
+ '@asamuzakjp/css-color@5.1.11':
+ dependencies:
+ '@asamuzakjp/generational-cache': 1.0.1
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
'@asamuzakjp/dom-selector@6.5.6':
dependencies:
'@asamuzakjp/nwsapi': 2.3.9
@@ -26777,6 +26913,16 @@ snapshots:
is-potential-custom-element-name: 1.0.1
lru-cache: 11.2.2
+ '@asamuzakjp/dom-selector@7.1.1':
+ dependencies:
+ '@asamuzakjp/generational-cache': 1.0.1
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.2.1
+ is-potential-custom-element-name: 1.0.1
+
+ '@asamuzakjp/generational-cache@1.0.1': {}
+
'@asamuzakjp/nwsapi@2.3.9': {}
'@assemblyscript/loader@0.19.23': {}
@@ -27320,6 +27466,10 @@ snapshots:
'@braidai/lang@1.1.2': {}
+ '@bramus/specificity@2.4.2':
+ dependencies:
+ css-tree: 3.1.0
+
'@bundled-es-modules/cookie@2.0.1':
dependencies:
cookie: 0.7.2
@@ -27653,7 +27803,7 @@ snapshots:
'@codspeed/core': 5.5.0
tinybench: 2.9.0
vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)
- vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
transitivePeerDependencies:
- debug
- supports-color
@@ -27694,6 +27844,8 @@ snapshots:
'@csstools/color-helpers@5.1.0': {}
+ '@csstools/color-helpers@6.0.2': {}
+
'@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
@@ -27704,6 +27856,11 @@ snapshots:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
+ '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
'@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
dependencies:
'@csstools/color-helpers': 5.0.1
@@ -27718,6 +27875,13 @@ snapshots:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
+ '@csstools/css-color-parser@4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/color-helpers': 6.0.2
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
'@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)':
dependencies:
'@csstools/css-tokenizer': 3.0.3
@@ -27726,14 +27890,24 @@ snapshots:
dependencies:
'@csstools/css-tokenizer': 3.0.4
+ '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-tokenizer': 4.0.0
+
'@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.15)':
dependencies:
postcss: 8.5.15
+ '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)':
+ optionalDependencies:
+ css-tree: 3.2.1
+
'@csstools/css-tokenizer@3.0.3': {}
'@csstools/css-tokenizer@3.0.4': {}
+ '@csstools/css-tokenizer@4.0.0': {}
+
'@dabh/diagnostics@2.0.8':
dependencies:
'@so-ric/colorspace': 1.1.6
@@ -28420,6 +28594,10 @@ snapshots:
'@eslint/core': 0.12.0
levn: 0.4.1
+ '@exodus/bytes@1.15.1(@noble/hashes@2.0.1)':
+ optionalDependencies:
+ '@noble/hashes': 2.0.1
+
'@fastify/accept-negotiator@2.0.1': {}
'@fastify/ajv-compiler@4.0.5':
@@ -31517,9 +31695,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21))':
+ '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23))':
dependencies:
- '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0)
+ '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0)
react-refresh: 0.18.0
optionalDependencies:
'@rsbuild/core': 2.0.1
@@ -31710,13 +31888,6 @@ snapshots:
optionalDependencies:
'@swc/helpers': 0.5.23
- '@rspack/core@2.0.5(@swc/helpers@0.5.21)':
- dependencies:
- '@rspack/binding': 2.0.5
- optionalDependencies:
- '@swc/helpers': 0.5.21
- optional: true
-
'@rspack/core@2.0.5(@swc/helpers@0.5.23)':
dependencies:
'@rspack/binding': 2.0.5
@@ -31725,12 +31896,6 @@ snapshots:
'@rspack/lite-tapable@1.1.0': {}
- '@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0)':
- dependencies:
- react-refresh: 0.18.0
- optionalDependencies:
- '@rspack/core': 2.0.5(@swc/helpers@0.5.21)
-
'@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0)':
dependencies:
react-refresh: 0.18.0
@@ -33610,7 +33775,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.1.0
- vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@25.0.1)(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
'@vitest/utils@4.1.4':
dependencies:
@@ -35076,6 +35241,11 @@ snapshots:
mdn-data: 2.12.2
source-map-js: 1.2.1
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
css-what@6.1.0: {}
css.escape@1.5.1: {}
@@ -35117,6 +35287,13 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 15.1.0
+ data-urls@7.0.0(@noble/hashes@2.0.1):
+ dependencies:
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1(@noble/hashes@2.0.1)
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
dataloader@1.4.0: {}
date-fns@2.30.0:
@@ -35145,6 +35322,8 @@ snapshots:
decimal.js@10.5.0: {}
+ decimal.js@10.6.0: {}
+
dedent@1.5.1(babel-plugin-macros@3.1.0):
optionalDependencies:
babel-plugin-macros: 3.1.0
@@ -35468,6 +35647,8 @@ snapshots:
entities@6.0.0: {}
+ entities@8.0.0: {}
+
env-paths@2.2.1: {}
env-paths@3.0.0: {}
@@ -36683,6 +36864,12 @@ snapshots:
dependencies:
whatwg-encoding: 3.1.1
+ html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
+ dependencies:
+ '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1)
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
html-entities@2.3.3: {}
html-link-extractor@1.0.5:
@@ -37259,6 +37446,32 @@ snapshots:
- supports-color
- utf-8-validate
+ jsdom@29.1.1(@noble/hashes@2.0.1):
+ dependencies:
+ '@asamuzakjp/css-color': 5.1.11
+ '@asamuzakjp/dom-selector': 7.1.1
+ '@bramus/specificity': 2.4.2
+ '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1)
+ '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1)
+ css-tree: 3.2.1
+ data-urls: 7.0.0(@noble/hashes@2.0.1)
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1)
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.5.1
+ parse5: 8.0.1
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.1
+ undici: 7.27.2
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.1
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1(@noble/hashes@2.0.1)
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -37560,6 +37773,8 @@ snapshots:
lru-cache@11.2.2: {}
+ lru-cache@11.5.1: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -37620,6 +37835,8 @@ snapshots:
mdn-data@2.12.2: {}
+ mdn-data@2.27.1: {}
+
media-typer@0.3.0: {}
media-typer@1.1.0: {}
@@ -37863,7 +38080,7 @@ snapshots:
nf3@0.3.6: {}
- nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
+ nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
dependencies:
consola: 3.4.2
crossws: 0.4.5(srvx@0.11.15)
@@ -37878,7 +38095,7 @@ snapshots:
rolldown: 1.0.2
srvx: 0.11.15
unenv: 2.0.0-rc.24
- unstorage: 2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3)
+ unstorage: 2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3)
optionalDependencies:
dotenv: 17.4.2
giget: 2.0.0
@@ -37915,7 +38132,7 @@ snapshots:
- sqlite3
- uploadthing
- nitro@3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
+ nitro@3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
dependencies:
consola: 3.4.2
crossws: 0.4.3(srvx@0.10.1)
@@ -37930,7 +38147,7 @@ snapshots:
srvx: 0.10.1
undici: 7.24.4
unenv: 2.0.0-rc.24
- unstorage: 2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3)
+ unstorage: 2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3)
optionalDependencies:
rolldown: 1.0.2
rollup: 4.56.0
@@ -37966,7 +38183,7 @@ snapshots:
- sqlite3
- uploadthing
- nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
+ nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
dependencies:
consola: 3.4.2
crossws: 0.4.4(srvx@0.11.15)
@@ -37981,7 +38198,7 @@ snapshots:
rolldown: 1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
srvx: 0.11.15
unenv: 2.0.0-rc.24
- unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3)
+ unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3)
optionalDependencies:
dotenv: 17.4.2
giget: 2.0.0
@@ -38636,6 +38853,10 @@ snapshots:
dependencies:
entities: 6.0.0
+ parse5@8.0.1:
+ dependencies:
+ entities: 8.0.0
+
parseurl@1.3.3: {}
pascal-case@3.1.2:
@@ -40281,6 +40502,10 @@ snapshots:
dependencies:
tldts: 7.0.16
+ tough-cookie@6.0.1:
+ dependencies:
+ tldts: 7.0.16
+
tr46@0.0.3: {}
tr46@5.0.0:
@@ -40456,6 +40681,8 @@ snapshots:
undici@7.24.4: {}
+ undici@7.27.2: {}
+
unenv@2.0.0-rc.24:
dependencies:
pathe: 2.0.3
@@ -40557,27 +40784,29 @@ snapshots:
db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3)
ioredis: 5.9.2
- unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3):
+ unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3):
optionalDependencies:
'@netlify/blobs': 10.1.0
chokidar: 5.0.0
db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3)
ioredis: 5.9.2
- lru-cache: 11.2.2
+ lru-cache: 11.5.1
ofetch: 2.0.0-alpha.3
- unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3):
+ unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3):
optionalDependencies:
'@netlify/blobs': 10.1.0
chokidar: 5.0.0
db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3)
+ lru-cache: 11.5.1
ofetch: 2.0.0-alpha.3
- unstorage@2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3):
+ unstorage@2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3):
optionalDependencies:
'@netlify/blobs': 10.1.0
chokidar: 5.0.0
db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3)
+ lru-cache: 11.5.1
ofetch: 2.0.0-alpha.3
untun@0.1.3:
@@ -40795,10 +41024,10 @@ snapshots:
transitivePeerDependencies:
- msw
- vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
+ vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.4
- '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.4
'@vitest/runner': 4.1.4
'@vitest/snapshot': 4.1.4
@@ -40824,7 +41053,36 @@ snapshots:
transitivePeerDependencies:
- msw
- vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
+ vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
+ dependencies:
+ '@vitest/expect': 4.1.4
+ '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
+ '@vitest/pretty-format': 4.1.4
+ '@vitest/runner': 4.1.4
+ '@vitest/snapshot': 4.1.4
+ '@vitest/spy': 4.1.4
+ '@vitest/utils': 4.1.4
+ es-module-lexer: 2.0.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 4.1.0
+ tinybench: 2.9.0
+ tinyexec: 1.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.1.0
+ vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 25.0.9
+ '@vitest/ui': 4.1.4(vitest@4.1.4)
+ jsdom: 29.1.1(@noble/hashes@2.0.1)
+ transitivePeerDependencies:
+ - msw
+
+ vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.4
'@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))
@@ -40849,7 +41107,7 @@ snapshots:
optionalDependencies:
'@types/node': 25.0.9
'@vitest/ui': 4.1.4(vitest@4.1.4)
- jsdom: 27.0.0(postcss@8.5.15)
+ jsdom: 29.1.1(@noble/hashes@2.0.1)
transitivePeerDependencies:
- msw
@@ -40990,6 +41248,8 @@ snapshots:
webidl-conversions@8.0.0: {}
+ webidl-conversions@8.0.1: {}
+
webpack-cli@5.1.4(webpack-dev-server@5.2.4)(webpack@5.97.1):
dependencies:
'@discoveryjs/json-ext': 0.5.7
@@ -41181,6 +41441,8 @@ snapshots:
whatwg-mimetype@4.0.0: {}
+ whatwg-mimetype@5.0.0: {}
+
whatwg-url@14.1.0:
dependencies:
tr46: 5.0.0
@@ -41191,6 +41453,14 @@ snapshots:
tr46: 6.0.0
webidl-conversions: 8.0.0
+ whatwg-url@16.0.1(@noble/hashes@2.0.1):
+ dependencies:
+ '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1)
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
From 9bd39c0feb84565c0432ab13874b07a8b268ffda Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 14:43:10 +0200
Subject: [PATCH 09/24] direct pprof calls for cleaner output
---
benchmarks/memory/README.md | 12 ++-
benchmarks/memory/client/flame-runner.ts | 2 +-
benchmarks/memory/client/package.json | 1 +
benchmarks/memory/flame-control.ts | 115 ++++++++++++++++++-----
benchmarks/memory/run-flame.mjs | 23 +++--
benchmarks/memory/server/package.json | 1 +
pnpm-lock.yaml | 6 ++
7 files changed, 121 insertions(+), 39 deletions(-)
diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md
index d6c007aa43..167ab28f27 100644
--- a/benchmarks/memory/README.md
+++ b/benchmarks/memory/README.md
@@ -118,7 +118,8 @@ pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemo
```
Local attribution profiling, without CodSpeed CLI/login/sudo/upload, uses
-`@platformatic/flame` heap profiles. These targets rebuild the scenarios with
+`@datadog/pprof` heap sampling and `@platformatic/flame` only to render the
+captured pprof files as HTML/Markdown. These targets rebuild the scenarios with
`--sourcemap true` so the generated profile reports can point back to source;
the normal CodSpeed benchmark builds are unchanged. Local aggregate scripts run
with `--parallel=1`, and scenario `test:flame` targets opt out of Nx parallelism
@@ -144,10 +145,11 @@ directory, including `heap-profile-*.html` and `heap-profile-*.md`. The
but manually start profiling after sanity/setup work and stop it after the
measured workload. Treat these profiles as diagnostic heap-sampling attribution;
they are not CodSpeed memory metrics such as peak memory, allocated bytes, or
-allocation counts. Reports can include `@platformatic/flame` and pprof shutdown
-frames. Flame runs do not force GC before profiling; doing so would perturb the
-workload and still would not make heap sampling equivalent to CodSpeed memory
-metrics.
+allocation counts. The heap sampler is stopped before profile conversion and
+Flame report generation, so Flame/pprof report-generation work should not appear
+as part of the captured workload. Flame runs do not force GC before profiling;
+doing so would perturb the workload and still would not make heap sampling
+equivalent to CodSpeed memory metrics.
Clean local Flame profile output with:
diff --git a/benchmarks/memory/client/flame-runner.ts b/benchmarks/memory/client/flame-runner.ts
index 504e4cda7d..93abe6cf13 100644
--- a/benchmarks/memory/client/flame-runner.ts
+++ b/benchmarks/memory/client/flame-runner.ts
@@ -8,7 +8,7 @@ export async function runClientFlameBenchmark(
const test = setup()
try {
- await setup().sanity()
+ await test.sanity()
await test.before?.()
await profileFlameWorkload(test.run)
} finally {
diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json
index 3602143c57..abee683e8c 100644
--- a/benchmarks/memory/client/package.json
+++ b/benchmarks/memory/client/package.json
@@ -27,6 +27,7 @@
},
"devDependencies": {
"@codspeed/vitest-plugin": "^5.5.0",
+ "@datadog/pprof": "^5.13.2",
"@platformatic/flame": "^1.6.0",
"@testing-library/react": "^16.2.0",
"@vitejs/plugin-react": "^6.0.1",
diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts
index e3aa127033..4ebfe3c791 100644
--- a/benchmarks/memory/flame-control.ts
+++ b/benchmarks/memory/flame-control.ts
@@ -1,49 +1,114 @@
+import fs from 'node:fs'
+import { createRequire } from 'node:module'
+import path from 'node:path'
+
const flameEnabled = process.env.TSR_MEMORY_FLAME === '1'
-const flameStartDelayMs = 250
+const heapIntervalBytes = 512 * 1024
+const heapStackDepth = 64
-function wait(ms: number) {
- return new Promise((resolve) => {
- setTimeout(resolve, ms)
- })
+interface HeapProfile {
+ encode: () => Uint8Array
}
-function waitForSignalHandler() {
- return wait(0)
+interface HeapProfiler {
+ start: (intervalBytes: number, stackDepth: number) => void
+ stop: () => void
+ v8Profile: () => unknown
+ convertProfile: (
+ v8Profile: unknown,
+ ignoreSamplePath?: string,
+ sourceMapper?: unknown,
+ ) => HeapProfile
}
-async function toggleFlameProfile() {
- if (!flameEnabled) {
- return
- }
+interface SourceMapperConstructor {
+ create: (searchDirs: Array) => Promise
+}
- if (process.platform === 'win32') {
- throw new Error('Flame manual profiling is not supported on Windows')
- }
+interface PprofModule {
+ heap: HeapProfiler
+ SourceMapper: SourceMapperConstructor
+}
- process.kill(process.pid, 'SIGUSR2')
- await waitForSignalHandler()
+function loadPprof() {
+ const requireFrom = process.env.TSR_MEMORY_REQUIRE_FROM
+ ? path.resolve(process.env.TSR_MEMORY_REQUIRE_FROM)
+ : import.meta.url
+ const require = createRequire(requireFrom)
+
+ return require('@datadog/pprof') as PprofModule
+}
+
+function getSourcemapDirs() {
+ return (process.env.TSR_MEMORY_SOURCEMAP_DIRS ?? '')
+ .split(path.delimiter)
+ .filter(Boolean)
}
-export async function startFlameProfile() {
- if (flameEnabled) {
- // Flame initializes sourcemap support asynchronously when the process starts.
- await wait(flameStartDelayMs)
+async function createSourceMapper(pprof: PprofModule) {
+ const sourcemapDirs = getSourcemapDirs()
+
+ if (sourcemapDirs.length === 0) {
+ return undefined
}
- await toggleFlameProfile()
+ return pprof.SourceMapper.create(sourcemapDirs)
}
-export async function stopFlameProfile() {
- await toggleFlameProfile()
+function formatTimestamp() {
+ return new Date().toISOString().replace(/[:.]/g, '-')
}
export async function profileFlameWorkload(
workload: () => Promise | void,
) {
- await startFlameProfile()
+ if (!flameEnabled) {
+ await workload()
+ return
+ }
+
+ const profileDir = process.env.TSR_MEMORY_PROFILE_DIR
+
+ if (!profileDir) {
+ throw new Error('TSR_MEMORY_PROFILE_DIR must be set for flame profiling')
+ }
+
+ fs.mkdirSync(profileDir, { recursive: true })
+
+ const pprof = loadPprof()
+ const sourceMapper = await createSourceMapper(pprof)
+
+ pprof.heap.start(heapIntervalBytes, heapStackDepth)
+
+ let workloadError: unknown = undefined
+ let v8Profile: unknown = undefined
+
try {
await workload()
+ } catch (error) {
+ workloadError = error
} finally {
- await stopFlameProfile()
+ try {
+ v8Profile = pprof.heap.v8Profile()
+ } finally {
+ pprof.heap.stop()
+ }
+ }
+
+ const heapProfile = pprof.heap.convertProfile(
+ v8Profile,
+ undefined,
+ sourceMapper,
+ )
+ const profilePath = path.join(
+ profileDir,
+ `heap-profile-${formatTimestamp()}.pb`,
+ )
+
+ fs.writeFileSync(profilePath, heapProfile.encode())
+ console.log(`Heap profile written to: ${profilePath}`)
+
+ if (workloadError) {
+ throw workloadError
}
}
diff --git a/benchmarks/memory/run-flame.mjs b/benchmarks/memory/run-flame.mjs
index f999be5340..cc568f3f77 100644
--- a/benchmarks/memory/run-flame.mjs
+++ b/benchmarks/memory/run-flame.mjs
@@ -1,4 +1,5 @@
#!/usr/bin/env node
+import { spawn } from 'node:child_process'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
@@ -35,22 +36,28 @@ const profileDir = path.join(
fs.mkdirSync(profileDir, { recursive: true })
const entrypointRequire = createRequire(entrypointPath)
-const { generateFlamegraph, generateMarkdown, startProfiling } =
- entrypointRequire('@platformatic/flame')
+const { generateFlamegraph, generateMarkdown } = entrypointRequire(
+ '@platformatic/flame',
+)
process.env.NODE_ENV = 'production'
-process.env.TSR_MEMORY_FLAME = '1'
console.log(`Flame profile directory: ${profileDir}`)
-const { pid, process: childProcess } = startProfiling(entrypointPath, [], {
- autoStart: false,
+const childProcess = spawn(process.execPath, [entrypointPath], {
cwd: profileDir,
- mdFormat: 'detailed',
- sourcemapDirs: [sourcemapDirPath],
+ env: {
+ ...process.env,
+ NODE_ENV: 'production',
+ TSR_MEMORY_FLAME: '1',
+ TSR_MEMORY_PROFILE_DIR: profileDir,
+ TSR_MEMORY_REQUIRE_FROM: entrypointPath,
+ TSR_MEMORY_SOURCEMAP_DIRS: sourcemapDirPath,
+ },
+ stdio: 'inherit',
})
-console.log(`Flame profiling process: ${pid}`)
+console.log(`Flame profiling process: ${childProcess.pid}`)
const childCode = await new Promise((resolve) => {
childProcess.on('error', (error) => {
diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json
index 11ec470bd9..f9fcae7cc8 100644
--- a/benchmarks/memory/server/package.json
+++ b/benchmarks/memory/server/package.json
@@ -18,6 +18,7 @@
},
"devDependencies": {
"@codspeed/vitest-plugin": "^5.5.0",
+ "@datadog/pprof": "^5.13.2",
"@platformatic/flame": "^1.6.0",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dde49c342a..2b2d43c119 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -331,6 +331,9 @@ importers:
'@codspeed/vitest-plugin':
specifier: ^5.5.0
version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)
+ '@datadog/pprof':
+ specifier: ^5.13.2
+ version: 5.13.2
'@platformatic/flame':
specifier: ^1.6.0
version: 1.6.0
@@ -374,6 +377,9 @@ importers:
'@codspeed/vitest-plugin':
specifier: ^5.5.0
version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)
+ '@datadog/pprof':
+ specifier: ^5.13.2
+ version: 5.13.2
'@platformatic/flame':
specifier: ^1.6.0
version: 1.6.0
From 3e63fd2c101edb757f163115ce544c9b82f9cc19 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 15:29:26 +0200
Subject: [PATCH 10/24] review
---
benchmarks/memory/README.md | 2 +-
benchmarks/memory/server/bench-utils.ts | 26 +++++++++-----
.../scenarios/aborted-requests/react/setup.ts | 4 ++-
.../scenarios/error-paths/react/setup.ts | 34 ++++++++++++++-----
.../scenarios/request-churn/react/setup.ts | 8 +++--
5 files changed, 53 insertions(+), 21 deletions(-)
diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md
index 167ab28f27..9cea416f0b 100644
--- a/benchmarks/memory/README.md
+++ b/benchmarks/memory/README.md
@@ -2,7 +2,7 @@
Dedicated memory benchmarks for TanStack Router / Start, measured with the
CodSpeed **memory instrument** (`mode: memory` in
-`.github/workflows/memory-benchmarks.yml`). Two separate benchmarks:
+`.github/workflows/client-nav-benchmarks.yml`). Two separate benchmarks:
- `server/` (`@benchmarks/memory-server`) — React Start apps, requests against
the built server handler (`handler.fetch`), Node environment.
diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts
index 4614152cc2..ac4cdd082d 100644
--- a/benchmarks/memory/server/bench-utils.ts
+++ b/benchmarks/memory/server/bench-utils.ts
@@ -2,8 +2,17 @@ export interface StartRequestHandler {
fetch: (request: Request) => Promise | Response
}
-export interface RunSequentialRequestLoopOptions {
- seed: number
+type RunSequentialRequestLoopRandomOptions =
+ | {
+ seed: number
+ random?: never
+ }
+ | {
+ random: () => number
+ seed?: never
+ }
+
+export type RunSequentialRequestLoopOptions = RunSequentialRequestLoopRandomOptions & {
iterations?: number
buildRequest: (random: () => number, index: number) => Request
validateResponse?: (response: Response, request: Request) => void
@@ -51,14 +60,13 @@ export async function drainResponse(response: Response) {
export async function runSequentialRequestLoop(
handler: StartRequestHandler,
- {
- seed,
- iterations = 10,
- buildRequest,
- validateResponse,
- }: RunSequentialRequestLoopOptions,
+ options: RunSequentialRequestLoopOptions,
) {
- const random = createDeterministicRandom(seed)
+ const { iterations = 10, buildRequest, validateResponse } = options
+ const random =
+ options.seed !== undefined
+ ? createDeterministicRandom(options.seed)
+ : options.random
const validate =
validateResponse ??
((response: Response, request: Request) => {
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
index 404b8410c2..4c453a5245 100644
--- a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
@@ -2,6 +2,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
const abortedRequestIterations = 100
+let abortedRequestCounter = 0
const eagerMarker = 'data-bench="aborted-requests-eager"'
const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"'
const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"'
@@ -130,7 +131,8 @@ async function assertAbortedRequestsSanity(handler: StartRequestHandler) {
async function runAbortedRequestLoop(handler: StartRequestHandler) {
for (let index = 0; index < abortedRequestIterations; index++) {
const controller = new AbortController()
- const request = buildStreamRequest(`abort-${index}`, controller.signal)
+ const id = `abort-${(abortedRequestCounter++).toString(36)}`
+ const request = buildStreamRequest(id, controller.signal)
const response = await handler.fetch(request)
validateDocumentResponse(response, request)
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
index d51bebd948..de256d4686 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
@@ -1,4 +1,5 @@
import {
+ createDeterministicRandom,
randomSegment,
runSequentialRequestLoop,
} from '#memory-server/bench-utils'
@@ -10,6 +11,15 @@ const redirectSeed = 0xdecafbad
const notFoundSeed = 0xdecafb0d
const errorSeed = 0xdecafbed
const unmatchedSeed = 0xdecaf00d
+// Module-level so each error-path bench keeps advancing across runner invocations.
+const redirectRandom = createDeterministicRandom(redirectSeed)
+const notFoundRandom = createDeterministicRandom(notFoundSeed)
+const errorRandom = createDeterministicRandom(errorSeed)
+const unmatchedRandom = createDeterministicRandom(unmatchedSeed)
+let redirectCounter = 0
+let notFoundCounter = 0
+let errorCounter = 0
+let unmatchedCounter = 0
const redirectStatus = 302
const notFoundStatus = 404
@@ -25,29 +35,37 @@ const requestInit = {
} satisfies RequestInit
function buildRedirectRequest(random: () => number) {
+ const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}`
+
return new Request(
- `http://localhost/from/${randomSegment(random)}`,
+ `http://localhost/from/${id}`,
requestInit,
)
}
function buildNotFoundRequest(random: () => number) {
+ const id = `${(notFoundCounter++).toString(36)}-${randomSegment(random)}`
+
return new Request(
- `http://localhost/missing/${randomSegment(random)}`,
+ `http://localhost/missing/${id}`,
requestInit,
)
}
function buildErrorRequest(random: () => number) {
+ const id = `${(errorCounter++).toString(36)}-${randomSegment(random)}`
+
return new Request(
- `http://localhost/boom/${randomSegment(random)}`,
+ `http://localhost/boom/${id}`,
requestInit,
)
}
function buildUnmatchedRequest(random: () => number) {
+ const id = `${(unmatchedCounter++).toString(36)}-${randomSegment(random)}`
+
return new Request(
- `http://localhost/nope/${randomSegment(random)}`,
+ `http://localhost/nope/${id}`,
requestInit,
)
}
@@ -152,7 +170,7 @@ export async function setup() {
const runRedirect = () =>
runSequentialRequestLoop(handler, {
- seed: redirectSeed,
+ random: redirectRandom,
iterations: errorPathsIterations,
buildRequest: buildRedirectRequest,
validateResponse: validateRedirectResponse,
@@ -160,7 +178,7 @@ export async function setup() {
const runNotFound = () =>
runSequentialRequestLoop(handler, {
- seed: notFoundSeed,
+ random: notFoundRandom,
iterations: errorPathsIterations,
buildRequest: buildNotFoundRequest,
validateResponse: validateNotFoundResponse,
@@ -168,7 +186,7 @@ export async function setup() {
const runError = () =>
runSequentialRequestLoop(handler, {
- seed: errorSeed,
+ random: errorRandom,
iterations: errorPathsIterations,
buildRequest: buildErrorRequest,
validateResponse: validateErrorResponse,
@@ -176,7 +194,7 @@ export async function setup() {
const runUnmatched = () =>
runSequentialRequestLoop(handler, {
- seed: unmatchedSeed,
+ random: unmatchedRandom,
iterations: errorPathsIterations,
buildRequest: buildUnmatchedRequest,
validateResponse: validateNotFoundResponse,
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
index 733578ce7f..13cdfa8bc5 100644
--- a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
@@ -1,4 +1,5 @@
import {
+ createDeterministicRandom,
randomSegment,
runSequentialRequestLoop,
} from '#memory-server/bench-utils'
@@ -9,6 +10,9 @@ const benchmarkSeed = 0xdecafbad
const requestChurnIterations = 200
const itemPageMarker = 'data-bench="request-churn-item"'
const dehydrationMarker = '$_TSR'
+// Module-level so CodSpeed warmups and measurement never replay URLs.
+const benchmarkRandom = createDeterministicRandom(benchmarkSeed)
+let requestCounter = 0
const requestInit = {
method: 'GET',
@@ -18,7 +22,7 @@ const requestInit = {
} satisfies RequestInit
function buildItemRequest(random: () => number) {
- const id = randomSegment(random)
+ const id = `${(requestCounter++).toString(36)}-${randomSegment(random)}`
const q = `q-${randomSegment(random)}`
return new Request(`http://localhost/items/${id}?q=${q}`, requestInit)
@@ -66,7 +70,7 @@ export async function setup() {
const run = () =>
runSequentialRequestLoop(handler, {
- seed: benchmarkSeed,
+ random: benchmarkRandom,
iterations: requestChurnIterations,
buildRequest: buildItemRequest,
validateResponse: validateItemResponse,
From 073a0364e853e54cdc505a18c54e22c701fb6222 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 15:41:35 +0200
Subject: [PATCH 11/24] increase iteration count for low benches
---
.../memory/server/scenarios/peak-large-page/react/setup.ts | 2 +-
.../server/scenarios/serialization-payload/react/setup.ts | 2 +-
.../memory/server/scenarios/streaming-peak/react/setup.ts | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
index e10d034aa2..bbe9fb51d9 100644
--- a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
@@ -3,7 +3,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
const benchmarkSeed = 0x5eed_0005
-const peakLargePageIterations = 2
+const peakLargePageIterations = 20
const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8'
const levelEightMarker = 'data-bench="peak-large-page-level-8"'
const knownDehydratedRecordName = 'peak-large-page-l8-record-199'
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
index f9242dd942..dd33533200 100644
--- a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
@@ -6,7 +6,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
const benchmarkSeed = 0x51eaa11
-const serializationPayloadIterations = 5
+const serializationPayloadIterations = 20
const payloadPageMarker = 'data-bench="serialization-payload"'
const dehydrationMarker = '$_TSR'
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
index 75657e5998..74b2f49271 100644
--- a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
@@ -6,7 +6,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
const benchmarkSeed = 0xdecafbad
-const streamingPeakIterations = 3
+const streamingPeakIterations = 20
const fallbackMarkers = [
'streaming-peak-fallback-0',
'streaming-peak-fallback-1',
From d8a4e76a3e7b161d1b89085fb84a81b1ce28e64c Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 16:03:29 +0200
Subject: [PATCH 12/24] flame run splits by benches, like codspeed
---
benchmarks/memory/flame-control.ts | 14 +++++++++++++-
benchmarks/memory/server/benchmark.ts | 1 -
benchmarks/memory/server/flame-runner.ts | 5 ++++-
.../scenarios/aborted-requests/react/setup.ts | 1 -
.../server/scenarios/error-paths/react/setup.ts | 6 ------
.../scenarios/peak-large-page/react/setup.ts | 1 -
.../server/scenarios/request-churn/react/setup.ts | 1 -
.../scenarios/serialization-payload/react/setup.ts | 1 -
.../scenarios/server-fn-churn/react/setup.ts | 1 -
.../server/scenarios/streaming-peak/react/setup.ts | 1 -
10 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts
index 4ebfe3c791..fda037cddd 100644
--- a/benchmarks/memory/flame-control.ts
+++ b/benchmarks/memory/flame-control.ts
@@ -59,8 +59,20 @@ function formatTimestamp() {
return new Date().toISOString().replace(/[:.]/g, '-')
}
+function formatProfileFileName(profileName?: string) {
+ const profileNameSlug = profileName
+ ?.trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ const profileNamePart = profileNameSlug ? `${profileNameSlug}-` : ''
+
+ return `heap-profile-${profileNamePart}${formatTimestamp()}.pb`
+}
+
export async function profileFlameWorkload(
workload: () => Promise | void,
+ profileName?: string,
) {
if (!flameEnabled) {
await workload()
@@ -102,7 +114,7 @@ export async function profileFlameWorkload(
)
const profilePath = path.join(
profileDir,
- `heap-profile-${formatTimestamp()}.pb`,
+ formatProfileFileName(profileName),
)
fs.writeFileSync(profilePath, heapProfile.encode())
diff --git a/benchmarks/memory/server/benchmark.ts b/benchmarks/memory/server/benchmark.ts
index 714b16baa4..02379a8ecf 100644
--- a/benchmarks/memory/server/benchmark.ts
+++ b/benchmarks/memory/server/benchmark.ts
@@ -5,6 +5,5 @@ export interface ServerMemoryBench {
export interface ServerMemoryBenchmark {
sanity: () => Promise | void
- run: () => Promise | void
benches: Array
}
diff --git a/benchmarks/memory/server/flame-runner.ts b/benchmarks/memory/server/flame-runner.ts
index d2a3425b73..d1892815c9 100644
--- a/benchmarks/memory/server/flame-runner.ts
+++ b/benchmarks/memory/server/flame-runner.ts
@@ -7,5 +7,8 @@ export async function runServerFlameBenchmark(
const test = await setup()
await test.sanity()
- await profileFlameWorkload(test.run)
+
+ for (const bench of test.benches) {
+ await profileFlameWorkload(bench.run, bench.name)
+ }
}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
index 4c453a5245..b3f1fc047b 100644
--- a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
@@ -154,7 +154,6 @@ export async function setup() {
return {
sanity: () => assertAbortedRequestsSanity(handler),
- run,
benches: [
{
name: 'mem aborted-requests (react)',
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
index de256d4686..50288f4358 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
@@ -202,12 +202,6 @@ export async function setup() {
return {
sanity: () => assertErrorPathsSanity(handler),
- async run() {
- await runRedirect()
- await runNotFound()
- await runError()
- await runUnmatched()
- },
benches: [
{
name: 'mem error-paths redirect (react)',
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
index bbe9fb51d9..28936ea0c6 100644
--- a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
@@ -65,7 +65,6 @@ export async function setup() {
return {
sanity: () => assertPeakLargePageSanity(handler),
- run,
benches: [
{
name: 'mem peak-large-page (react)',
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
index 13cdfa8bc5..8da5590818 100644
--- a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
@@ -78,7 +78,6 @@ export async function setup() {
return {
sanity: () => assertRequestChurnSanity(handler),
- run,
benches: [
{
name: 'mem request-churn (react)',
diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
index dd33533200..b67c0089e4 100644
--- a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts
@@ -96,7 +96,6 @@ export async function setup() {
return {
sanity: () => assertSerializationPayloadSanity(handler),
- run,
benches: [
{
name: 'mem serialization-payload (react)',
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
index c93fc4ab9b..9aaf3e0256 100644
--- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
@@ -228,7 +228,6 @@ export async function setup() {
return {
sanity: () => assertServerFnChurnSanity(handler, urls),
- run,
benches: [
{
name: 'mem server-fn-churn (react)',
diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
index 74b2f49271..895520d45f 100644
--- a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts
@@ -136,7 +136,6 @@ export async function setup() {
return {
sanity: () => assertStreamingPeakSanity(handler),
- run,
benches: [
{
name: 'mem streaming-peak chunked (react)',
From d1ef03d77768d86859452c78aca19fd29efd9c7a Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Sat, 13 Jun 2026 14:04:50 +0000
Subject: [PATCH 13/24] ci: apply automated fixes
---
benchmarks/memory/flame-control.ts | 5 +----
benchmarks/memory/server/bench-utils.ts | 11 +++++-----
.../scenarios/error-paths/react/setup.ts | 20 ++++---------------
3 files changed, 11 insertions(+), 25 deletions(-)
diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts
index fda037cddd..0d6f42df33 100644
--- a/benchmarks/memory/flame-control.ts
+++ b/benchmarks/memory/flame-control.ts
@@ -112,10 +112,7 @@ export async function profileFlameWorkload(
undefined,
sourceMapper,
)
- const profilePath = path.join(
- profileDir,
- formatProfileFileName(profileName),
- )
+ const profilePath = path.join(profileDir, formatProfileFileName(profileName))
fs.writeFileSync(profilePath, heapProfile.encode())
console.log(`Heap profile written to: ${profilePath}`)
diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts
index ac4cdd082d..d1f1406b51 100644
--- a/benchmarks/memory/server/bench-utils.ts
+++ b/benchmarks/memory/server/bench-utils.ts
@@ -12,11 +12,12 @@ type RunSequentialRequestLoopRandomOptions =
seed?: never
}
-export type RunSequentialRequestLoopOptions = RunSequentialRequestLoopRandomOptions & {
- iterations?: number
- buildRequest: (random: () => number, index: number) => Request
- validateResponse?: (response: Response, request: Request) => void
-}
+export type RunSequentialRequestLoopOptions =
+ RunSequentialRequestLoopRandomOptions & {
+ iterations?: number
+ buildRequest: (random: () => number, index: number) => Request
+ validateResponse?: (response: Response, request: Request) => void
+ }
export const memoryBenchOptions = {
iterations: 1,
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
index 50288f4358..3e5b658ff6 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
@@ -37,37 +37,25 @@ const requestInit = {
function buildRedirectRequest(random: () => number) {
const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}`
- return new Request(
- `http://localhost/from/${id}`,
- requestInit,
- )
+ return new Request(`http://localhost/from/${id}`, requestInit)
}
function buildNotFoundRequest(random: () => number) {
const id = `${(notFoundCounter++).toString(36)}-${randomSegment(random)}`
- return new Request(
- `http://localhost/missing/${id}`,
- requestInit,
- )
+ return new Request(`http://localhost/missing/${id}`, requestInit)
}
function buildErrorRequest(random: () => number) {
const id = `${(errorCounter++).toString(36)}-${randomSegment(random)}`
- return new Request(
- `http://localhost/boom/${id}`,
- requestInit,
- )
+ return new Request(`http://localhost/boom/${id}`, requestInit)
}
function buildUnmatchedRequest(random: () => number) {
const id = `${(unmatchedCounter++).toString(36)}-${randomSegment(random)}`
- return new Request(
- `http://localhost/nope/${id}`,
- requestInit,
- )
+ return new Request(`http://localhost/nope/${id}`, requestInit)
}
function getRequestId(request: Request) {
From 2ff1a0de2a65e48f78b9020f2e3a154970a9787c Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 18:19:23 +0200
Subject: [PATCH 14/24] QA
---
benchmarks/memory/client/bench-utils.ts | 1 +
.../interrupted-navigations/react/setup.ts | 21 +++++++++++++++----
benchmarks/memory/client/vitest.setup.ts | 9 +++++---
benchmarks/memory/server/bench-utils.ts | 1 +
4 files changed, 25 insertions(+), 7 deletions(-)
diff --git a/benchmarks/memory/client/bench-utils.ts b/benchmarks/memory/client/bench-utils.ts
index 28bfe6c512..0cbaedbd71 100644
--- a/benchmarks/memory/client/bench-utils.ts
+++ b/benchmarks/memory/client/bench-utils.ts
@@ -3,6 +3,7 @@ export const memoryBenchOptions = {
warmupIterations: 1,
time: 0,
warmupTime: 0,
+ throws: true,
}
export function createDeterministicRandom(seed: number) {
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
index 320d4cfb0b..3f62369ec5 100644
--- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
@@ -82,6 +82,21 @@ function assertSlowNavigationSettlement(settlement: NavigationSettlement) {
)
}
+async function awaitExpectedLoadSettlement(loadPromise: Promise) {
+ try {
+ await loadPromise
+ } catch (reason) {
+ if (
+ reasonHasAbortShape(reason) ||
+ reasonHasCancellationShape(reason)
+ ) {
+ return
+ }
+
+ throw reason
+ }
+}
+
function reasonHasAbortShape(reason: unknown) {
return reason instanceof DOMException && reason.name === 'AbortError'
}
@@ -250,13 +265,11 @@ export function setup() {
resolveSlowLoader(slowId)
const settlement = await slowNavigation
- // Superseded loads currently always resolve; the guard keeps the bench
- // alive if router-core ever starts rejecting them.
- await slowLoadPromise.catch(() => undefined)
+ assertSlowNavigationSettlement(settlement)
+ await awaitExpectedLoadSettlement(slowLoadPromise)
await drainMicrotasks()
if (assertShape) {
- assertSlowNavigationSettlement(settlement)
assertRenderedPage('fast', fastId)
}
}
diff --git a/benchmarks/memory/client/vitest.setup.ts b/benchmarks/memory/client/vitest.setup.ts
index 9bac58bd41..ce3127275a 100644
--- a/benchmarks/memory/client/vitest.setup.ts
+++ b/benchmarks/memory/client/vitest.setup.ts
@@ -1,6 +1,9 @@
-import { vi } from 'vitest'
-
// @ts-expect-error
global.IS_REACT_ACT_ENVIRONMENT = true
-window.scrollTo = vi.fn()
+const scrollTo = () => {}
+
+window.scrollTo = scrollTo
+globalThis.scrollTo = scrollTo
+
+export {}
diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts
index d1f1406b51..f1f5dadea0 100644
--- a/benchmarks/memory/server/bench-utils.ts
+++ b/benchmarks/memory/server/bench-utils.ts
@@ -24,6 +24,7 @@ export const memoryBenchOptions = {
warmupIterations: 1,
time: 0,
warmupTime: 0,
+ throws: true,
}
export function createDeterministicRandom(seed: number) {
From 92c26889bf2b503c2cea345e2392c14e654cd684 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 18:27:45 +0200
Subject: [PATCH 15/24] build outside of codspeed instrumentation
---
.github/workflows/client-nav-benchmarks.yml | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml
index 5db0e1af9c..8ccb0d12cc 100644
--- a/.github/workflows/client-nav-benchmarks.yml
+++ b/.github/workflows/client-nav-benchmarks.yml
@@ -67,6 +67,13 @@ jobs:
- name: Setup Tools
uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main
+ # Run the build outside of the CodSpeed action to avoid being slowed by intrumentation overhead.
+ # (and then run the task itself with --excludeTaskDependencies)
+ - name: Prepare ${{ matrix.benchmark }}:${{ matrix.framework }} benchmark
+ run: >-
+ pnpm nx run
+ @benchmarks/${{ matrix.benchmark }}:build:${{ matrix.framework }}
+
- name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
with:
@@ -75,3 +82,4 @@ jobs:
WITH_INSTRUMENTATION=1
pnpm nx run
@benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }}
+ --excludeTaskDependencies
From fab18e6f11c76fbb34a0f2f8b0924d379ae08046 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Sat, 13 Jun 2026 16:29:52 +0000
Subject: [PATCH 16/24] ci: apply automated fixes
---
.../client/scenarios/interrupted-navigations/react/setup.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
index 3f62369ec5..d0532e87e6 100644
--- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
@@ -86,10 +86,7 @@ async function awaitExpectedLoadSettlement(loadPromise: Promise) {
try {
await loadPromise
} catch (reason) {
- if (
- reasonHasAbortShape(reason) ||
- reasonHasCancellationShape(reason)
- ) {
+ if (reasonHasAbortShape(reason) || reasonHasCancellationShape(reason)) {
return
}
From 6a4c1f6fae4ae6c9b8e6dc565c9b3c9871e76724 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 18:49:15 +0200
Subject: [PATCH 17/24] nitpick
---
.../memory/server/scenarios/server-fn-churn/react/setup.ts | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
index 9aaf3e0256..adc07b6b85 100644
--- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
@@ -215,13 +215,12 @@ export async function setup() {
if (index % 2 === 0) {
const fixture = getFixtures[fixtureIndex]!
-
return buildGetRequest(urls.get, fixture)
+ } else {
+ const fixture = postFixtures[fixtureIndex]!
+ return buildPostRequest(urls.post, fixture)
}
- const fixture = postFixtures[fixtureIndex]!
-
- return buildPostRequest(urls.post, fixture)
},
validateResponse: validateServerFnResponse,
})
From a30c37e7afd20a4a8649df2d8e4441cab60833d3 Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 18:52:17 +0200
Subject: [PATCH 18/24] fix workspace deps
---
benchmarks/memory/client/package.json | 4 ++--
benchmarks/memory/server/package.json | 4 ++--
.../memory/server/scenarios/server-fn-churn/react/setup.ts | 1 -
3 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json
index abee683e8c..534225d2a3 100644
--- a/benchmarks/memory/client/package.json
+++ b/benchmarks/memory/client/package.json
@@ -20,8 +20,8 @@
}
},
"dependencies": {
- "@tanstack/react-router": "workspace:^",
- "@tanstack/router-core": "workspace:^",
+ "@tanstack/react-router": "workspace:*",
+ "@tanstack/router-core": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json
index f9fcae7cc8..2427549e87 100644
--- a/benchmarks/memory/server/package.json
+++ b/benchmarks/memory/server/package.json
@@ -11,8 +11,8 @@
"#memory-server/runner": "./runner.ts"
},
"dependencies": {
- "@tanstack/react-router": "workspace:^",
- "@tanstack/react-start": "workspace:^",
+ "@tanstack/react-router": "workspace:*",
+ "@tanstack/react-start": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
index adc07b6b85..0469c70909 100644
--- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts
@@ -220,7 +220,6 @@ export async function setup() {
const fixture = postFixtures[fixtureIndex]!
return buildPostRequest(urls.post, fixture)
}
-
},
validateResponse: validateServerFnResponse,
})
From cbfde6df47025cd1a41246d8266f96c6f3f3e99a Mon Sep 17 00:00:00 2001
From: Sheraff
Date: Sat, 13 Jun 2026 22:32:59 +0200
Subject: [PATCH 19/24] solid & vue
---
.github/workflows/client-nav-benchmarks.yml | 10 -
benchmarks/memory/README.md | 34 +-
benchmarks/memory/client/package.json | 149 +++++++-
.../interrupted-navigations/react/setup.ts | 291 +--------------
.../interrupted-navigations/shared.ts | 339 ++++++++++++++++++
.../solid/memory.bench.ts | 9 +
.../solid/memory.flame.ts | 4 +
.../solid/project.json | 54 +++
.../interrupted-navigations/solid/setup.ts | 20 ++
.../interrupted-navigations/solid/src/app.tsx | 35 ++
.../solid/src/routeTree.gen.ts | 95 +++++
.../solid/src/router.tsx | 17 +
.../solid/src/routes/__root.tsx | 9 +
.../solid/src/routes/fast.$id.tsx | 21 ++
.../solid/src/routes/index.tsx | 9 +
.../solid/src/routes/slow.$id.tsx | 21 ++
.../solid/src/slow-loaders.ts | 54 +++
.../solid/tsconfig.json | 19 +
.../solid/vite.config.ts | 34 ++
.../vue/memory.bench.ts | 9 +
.../vue/memory.flame.ts | 4 +
.../interrupted-navigations/vue/project.json | 54 +++
.../interrupted-navigations/vue/setup.ts | 20 ++
.../interrupted-navigations/vue/src/app.tsx | 42 +++
.../vue/src/routeTree.gen.ts | 95 +++++
.../vue/src/router.tsx | 17 +
.../vue/src/routes/__root.tsx | 9 +
.../vue/src/routes/fast.$id.tsx | 21 ++
.../vue/src/routes/index.tsx | 9 +
.../vue/src/routes/slow.$id.tsx | 21 ++
.../vue/src/slow-loaders.ts | 54 +++
.../interrupted-navigations/vue/tsconfig.json | 19 +
.../vue/vite.config.ts | 36 ++
.../loader-data-retention/react/setup.ts | 162 +--------
.../scenarios/loader-data-retention/shared.ts | 205 +++++++++++
.../solid/memory.bench.ts | 9 +
.../solid/memory.flame.ts | 4 +
.../loader-data-retention/solid/project.json | 54 +++
.../loader-data-retention/solid/setup.ts | 11 +
.../loader-data-retention/solid/src/app.tsx | 31 ++
.../solid/src/loader-data.ts | 48 +++
.../solid/src/routeTree.gen.ts | 102 ++++++
.../solid/src/router.tsx | 19 +
.../solid/src/routes/__root.tsx | 9 +
.../solid/src/routes/page.$id.tsx | 21 ++
.../solid/src/routes/shell.index.tsx | 9 +
.../solid/src/routes/shell.tsx | 9 +
.../loader-data-retention/solid/tsconfig.json | 19 +
.../solid/vite.config.ts | 34 ++
.../loader-data-retention/vue/memory.bench.ts | 9 +
.../loader-data-retention/vue/memory.flame.ts | 4 +
.../loader-data-retention/vue/project.json | 54 +++
.../loader-data-retention/vue/setup.ts | 11 +
.../loader-data-retention/vue/src/app.tsx | 38 ++
.../vue/src/loader-data.ts | 48 +++
.../vue/src/routeTree.gen.ts | 102 ++++++
.../loader-data-retention/vue/src/router.tsx | 19 +
.../vue/src/routes/__root.tsx | 9 +
.../vue/src/routes/page.$id.tsx | 22 ++
.../vue/src/routes/shell.index.tsx | 9 +
.../vue/src/routes/shell.tsx | 9 +
.../loader-data-retention/vue/tsconfig.json | 19 +
.../loader-data-retention/vue/vite.config.ts | 36 ++
.../scenarios/mount-unmount/react/setup.ts | 66 +---
.../client/scenarios/mount-unmount/shared.ts | 90 +++++
.../mount-unmount/solid/memory.bench.ts | 9 +
.../mount-unmount/solid/memory.flame.ts | 4 +
.../mount-unmount/solid/project.json | 54 +++
.../scenarios/mount-unmount/solid/setup.ts | 11 +
.../scenarios/mount-unmount/solid/src/app.tsx | 27 ++
.../mount-unmount/solid/src/routeTree.gen.ts | 59 +++
.../mount-unmount/solid/src/router.tsx | 17 +
.../mount-unmount/solid/src/routes/__root.tsx | 9 +
.../mount-unmount/solid/src/routes/a.tsx | 12 +
.../mount-unmount/solid/tsconfig.json | 19 +
.../mount-unmount/solid/vite.config.ts | 34 ++
.../mount-unmount/vue/memory.bench.ts | 9 +
.../mount-unmount/vue/memory.flame.ts | 4 +
.../scenarios/mount-unmount/vue/project.json | 54 +++
.../scenarios/mount-unmount/vue/setup.ts | 11 +
.../scenarios/mount-unmount/vue/src/app.tsx | 34 ++
.../mount-unmount/vue/src/routeTree.gen.ts | 59 +++
.../mount-unmount/vue/src/router.tsx | 17 +
.../mount-unmount/vue/src/routes/__root.tsx | 9 +
.../mount-unmount/vue/src/routes/a.tsx | 12 +
.../scenarios/mount-unmount/vue/tsconfig.json | 19 +
.../mount-unmount/vue/vite.config.ts | 36 ++
.../scenarios/navigation-churn/react/setup.ts | 117 +-----
.../scenarios/navigation-churn/shared.ts | 141 ++++++++
.../navigation-churn/solid/memory.bench.ts | 9 +
.../navigation-churn/solid/memory.flame.ts | 4 +
.../navigation-churn/solid/project.json | 54 +++
.../scenarios/navigation-churn/solid/setup.ts | 11 +
.../navigation-churn/solid/src/app.tsx | 29 ++
.../solid/src/routeTree.gen.ts | 77 ++++
.../navigation-churn/solid/src/router.tsx | 17 +
.../solid/src/routes/__root.tsx | 9 +
.../navigation-churn/solid/src/routes/a.tsx | 14 +
.../navigation-churn/solid/src/routes/b.tsx | 14 +
.../navigation-churn/solid/tsconfig.json | 19 +
.../navigation-churn/solid/vite.config.ts | 34 ++
.../navigation-churn/vue/memory.bench.ts | 9 +
.../navigation-churn/vue/memory.flame.ts | 4 +
.../navigation-churn/vue/project.json | 54 +++
.../scenarios/navigation-churn/vue/setup.ts | 11 +
.../navigation-churn/vue/src/app.tsx | 36 ++
.../navigation-churn/vue/src/routeTree.gen.ts | 77 ++++
.../navigation-churn/vue/src/router.tsx | 17 +
.../vue/src/routes/__root.tsx | 9 +
.../navigation-churn/vue/src/routes/a.tsx | 14 +
.../navigation-churn/vue/src/routes/b.tsx | 14 +
.../navigation-churn/vue/tsconfig.json | 19 +
.../navigation-churn/vue/vite.config.ts | 36 ++
.../scenarios/preload-churn/react/setup.ts | 259 +------------
.../client/scenarios/preload-churn/shared.ts | 308 ++++++++++++++++
.../preload-churn/solid/memory.bench.ts | 9 +
.../preload-churn/solid/memory.flame.ts | 4 +
.../preload-churn/solid/project.json | 54 +++
.../scenarios/preload-churn/solid/setup.ts | 11 +
.../scenarios/preload-churn/solid/src/app.tsx | 31 ++
.../preload-churn/solid/src/item-payload.ts | 51 +++
.../preload-churn/solid/src/routeTree.gen.ts | 77 ++++
.../preload-churn/solid/src/router.tsx | 18 +
.../preload-churn/solid/src/routes/__root.tsx | 9 +
.../preload-churn/solid/src/routes/index.tsx | 9 +
.../solid/src/routes/items.$id.tsx | 20 ++
.../preload-churn/solid/tsconfig.json | 19 +
.../preload-churn/solid/vite.config.ts | 34 ++
.../preload-churn/vue/memory.bench.ts | 9 +
.../preload-churn/vue/memory.flame.ts | 4 +
.../scenarios/preload-churn/vue/project.json | 54 +++
.../scenarios/preload-churn/vue/setup.ts | 11 +
.../scenarios/preload-churn/vue/src/app.tsx | 38 ++
.../preload-churn/vue/src/item-payload.ts | 51 +++
.../preload-churn/vue/src/routeTree.gen.ts | 77 ++++
.../preload-churn/vue/src/router.tsx | 18 +
.../preload-churn/vue/src/routes/__root.tsx | 9 +
.../preload-churn/vue/src/routes/index.tsx | 9 +
.../vue/src/routes/items.$id.tsx | 20 ++
.../scenarios/preload-churn/vue/tsconfig.json | 19 +
.../preload-churn/vue/vite.config.ts | 36 ++
.../unique-location-churn/react/setup.ts | 134 +------
.../scenarios/unique-location-churn/shared.ts | 165 +++++++++
.../solid/memory.bench.ts | 9 +
.../solid/memory.flame.ts | 4 +
.../unique-location-churn/solid/project.json | 54 +++
.../unique-location-churn/solid/setup.ts | 11 +
.../unique-location-churn/solid/src/app.tsx | 29 ++
.../solid/src/routeTree.gen.ts | 59 +++
.../solid/src/router.tsx | 17 +
.../solid/src/routes/__root.tsx | 9 +
.../solid/src/routes/items.$id.tsx | 28 ++
.../unique-location-churn/solid/tsconfig.json | 19 +
.../solid/vite.config.ts | 34 ++
.../unique-location-churn/vue/memory.bench.ts | 9 +
.../unique-location-churn/vue/memory.flame.ts | 4 +
.../unique-location-churn/vue/project.json | 54 +++
.../unique-location-churn/vue/setup.ts | 11 +
.../unique-location-churn/vue/src/app.tsx | 36 ++
.../vue/src/routeTree.gen.ts | 59 +++
.../unique-location-churn/vue/src/router.tsx | 17 +
.../vue/src/routes/__root.tsx | 9 +
.../vue/src/routes/items.$id.tsx | 34 ++
.../unique-location-churn/vue/tsconfig.json | 19 +
.../unique-location-churn/vue/vite.config.ts | 36 ++
.../memory/client/vitest.solid.config.ts | 9 +
benchmarks/memory/client/vitest.vue.config.ts | 9 +
benchmarks/memory/server/package.json | 158 +++++++-
.../scenarios/aborted-requests/react/setup.ts | 156 +-------
.../scenarios/aborted-requests/shared.ts | 309 ++++++++++++++++
.../aborted-requests/solid/memory.bench.ts | 10 +
.../aborted-requests/solid/memory.flame.ts | 4 +
.../aborted-requests/solid/project.json | 54 +++
.../scenarios/aborted-requests/solid/setup.ts | 14 +
.../solid/src/routeTree.gen.ts | 86 +++++
.../aborted-requests/solid/src/router.tsx | 16 +
.../solid/src/routes/__root.tsx | 29 ++
.../solid/src/routes/index.tsx | 9 +
.../solid/src/routes/stream.$id.tsx | 133 +++++++
.../aborted-requests/solid/tsconfig.json | 17 +
.../aborted-requests/solid/vite.config.ts | 34 ++
.../aborted-requests/vue/memory.bench.ts | 10 +
.../aborted-requests/vue/memory.flame.ts | 4 +
.../aborted-requests/vue/project.json | 54 +++
.../scenarios/aborted-requests/vue/setup.ts | 14 +
.../aborted-requests/vue/src/routeTree.gen.ts | 86 +++++
.../aborted-requests/vue/src/router.tsx | 16 +
.../vue/src/routes/__root.tsx | 29 ++
.../aborted-requests/vue/src/routes/index.tsx | 9 +
.../vue/src/routes/stream.$id.tsx | 130 +++++++
.../aborted-requests/vue/tsconfig.json | 17 +
.../aborted-requests/vue/vite.config.ts | 29 ++
.../scenarios/error-paths/react/setup.ts | 204 +----------
.../server/scenarios/error-paths/shared.ts | 211 +++++++++++
.../error-paths/solid/memory.bench.ts | 10 +
.../error-paths/solid/memory.flame.ts | 4 +
.../scenarios/error-paths/solid/project.json | 54 +++
.../scenarios/error-paths/solid/setup.ts | 14 +
.../error-paths/solid/src/routeTree.gen.ts | 146 ++++++++
.../error-paths/solid/src/router.tsx | 23 ++
.../error-paths/solid/src/routes/__root.tsx | 29 ++
.../error-paths/solid/src/routes/boom.$id.tsx | 17 +
.../error-paths/solid/src/routes/from.$id.tsx | 14 +
.../error-paths/solid/src/routes/index.tsx | 9 +
.../solid/src/routes/missing.$id.tsx | 19 +
.../solid/src/routes/target.$id.tsx | 11 +
.../scenarios/error-paths/solid/tsconfig.json | 17 +
.../error-paths/solid/vite.config.ts | 34 ++
.../scenarios/error-paths/vue/memory.bench.ts | 10 +
.../scenarios/error-paths/vue/memory.flame.ts | 4 +
.../scenarios/error-paths/vue/project.json | 54 +++
.../server/scenarios/error-paths/vue/setup.ts | 14 +
.../error-paths/vue/src/routeTree.gen.ts | 146 ++++++++
.../scenarios/error-paths/vue/src/router.tsx | 16 +
.../error-paths/vue/src/routes/__root.tsx | 29 ++
.../error-paths/vue/src/routes/boom.$id.tsx | 17 +
.../error-paths/vue/src/routes/from.$id.tsx | 14 +
.../error-paths/vue/src/routes/index.tsx | 9 +
.../vue/src/routes/missing.$id.tsx | 19 +
.../error-paths/vue/src/routes/target.$id.tsx | 11 +
.../scenarios/error-paths/vue/tsconfig.json | 17 +
.../scenarios/error-paths/vue/vite.config.ts | 29 ++
.../scenarios/peak-large-page/react/setup.ts | 67 +---
.../scenarios/peak-large-page/shared.ts | 75 ++++
.../peak-large-page/solid/memory.bench.ts | 10 +
.../peak-large-page/solid/memory.flame.ts | 4 +
.../peak-large-page/solid/project.json | 54 +++
.../scenarios/peak-large-page/solid/setup.ts | 14 +
.../solid/src/large-page-data.ts | 111 ++++++
.../solid/src/routeTree.gen.ts | 305 ++++++++++++++++
.../peak-large-page/solid/src/router.tsx | 16 +
.../solid/src/routes/__root.tsx | 29 ++
.../solid/src/routes/index.tsx | 9 +
.../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 25 ++
.../solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 26 ++
.../solid/src/routes/l1.l2.l3.l4.l5.l6.tsx | 26 ++
.../solid/src/routes/l1.l2.l3.l4.l5.tsx | 26 ++
.../solid/src/routes/l1.l2.l3.l4.tsx | 26 ++
.../solid/src/routes/l1.l2.l3.tsx | 26 ++
.../solid/src/routes/l1.l2.tsx | 26 ++
.../peak-large-page/solid/src/routes/l1.tsx | 26 ++
.../peak-large-page/solid/tsconfig.json | 17 +
.../peak-large-page/solid/vite.config.ts | 34 ++
.../peak-large-page/vue/memory.bench.ts | 10 +
.../peak-large-page/vue/memory.flame.ts | 4 +
.../peak-large-page/vue/project.json | 54 +++
.../scenarios/peak-large-page/vue/setup.ts | 14 +
.../vue/src/large-page-data.ts | 111 ++++++
.../peak-large-page/vue/src/routeTree.gen.ts | 305 ++++++++++++++++
.../peak-large-page/vue/src/router.tsx | 16 +
.../peak-large-page/vue/src/routes/__root.tsx | 29 ++
.../peak-large-page/vue/src/routes/index.tsx | 9 +
.../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 25 ++
.../vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 26 ++
.../vue/src/routes/l1.l2.l3.l4.l5.l6.tsx | 26 ++
.../vue/src/routes/l1.l2.l3.l4.l5.tsx | 26 ++
.../vue/src/routes/l1.l2.l3.l4.tsx | 26 ++
.../vue/src/routes/l1.l2.l3.tsx | 26 ++
.../peak-large-page/vue/src/routes/l1.l2.tsx | 26 ++
.../peak-large-page/vue/src/routes/l1.tsx | 26 ++
.../peak-large-page/vue/tsconfig.json | 17 +
.../peak-large-page/vue/vite.config.ts | 29 ++
.../scenarios/request-churn/react/setup.ts | 80 +----
.../server/scenarios/request-churn/shared.ts | 89 +++++
.../request-churn/solid/memory.bench.ts | 10 +
.../request-churn/solid/memory.flame.ts | 4 +
.../request-churn/solid/project.json | 54 +++
.../scenarios/request-churn/solid/setup.ts | 14 +
.../request-churn/solid/src/routeTree.gen.ts | 86 +++++
.../request-churn/solid/src/router.tsx | 16 +
.../request-churn/solid/src/routes/__root.tsx | 29 ++
.../request-churn/solid/src/routes/index.tsx | 9 +
.../solid/src/routes/items.$id.tsx | 40 +++
.../request-churn/solid/tsconfig.json | 17 +
.../request-churn/solid/vite.config.ts | 34 ++
.../request-churn/vue/memory.bench.ts | 10 +
.../request-churn/vue/memory.flame.ts | 4 +
.../scenarios/request-churn/vue/project.json | 54 +++
.../scenarios/request-churn/vue/setup.ts | 14 +
.../request-churn/vue/src/routeTree.gen.ts | 86 +++++
.../request-churn/vue/src/router.tsx | 16 +
.../request-churn/vue/src/routes/__root.tsx | 29 ++
.../request-churn/vue/src/routes/index.tsx | 9 +
.../vue/src/routes/items.$id.tsx | 40 +++
.../scenarios/request-churn/vue/tsconfig.json | 17 +
.../request-churn/vue/vite.config.ts | 29 ++
.../serialization-payload/react/setup.ts | 98 +----
.../scenarios/serialization-payload/shared.ts | 106 ++++++
.../solid/memory.bench.ts | 10 +
.../solid/memory.flame.ts | 4 +
.../serialization-payload/solid/project.json | 54 +++
.../serialization-payload/solid/setup.ts | 14 +
.../solid/src/routeTree.gen.ts | 68 ++++
.../solid/src/router.tsx | 16 +
.../solid/src/routes/__root.tsx | 29 ++
.../solid/src/routes/data.$id.tsx | 132 +++++++
.../serialization-payload/solid/tsconfig.json | 17 +
.../solid/vite.config.ts | 34 ++
.../serialization-payload/vue/memory.bench.ts | 10 +
.../serialization-payload/vue/memory.flame.ts | 4 +
.../serialization-payload/vue/project.json | 54 +++
.../serialization-payload/vue/setup.ts | 14 +
.../vue/src/routeTree.gen.ts | 68 ++++
.../serialization-payload/vue/src/router.tsx | 16 +
.../vue/src/routes/__root.tsx | 29 ++
.../vue/src/routes/data.$id.tsx | 132 +++++++
.../serialization-payload/vue/tsconfig.json | 17 +
.../serialization-payload/vue/vite.config.ts | 29 ++
.../scenarios/server-fn-churn/react/setup.ts | 228 +-----------
.../scenarios/server-fn-churn/shared.ts | 236 ++++++++++++
.../server-fn-churn/solid/memory.bench.ts | 10 +
.../server-fn-churn/solid/memory.flame.ts | 4 +
.../server-fn-churn/solid/project.json | 54 +++
.../scenarios/server-fn-churn/solid/setup.ts | 14 +
.../server-fn-churn/solid/src/fns.ts | 47 +++
.../solid/src/routeTree.gen.ts | 86 +++++
.../server-fn-churn/solid/src/router.tsx | 16 +
.../solid/src/routes/__root.tsx | 29 ++
.../solid/src/routes/api.fn-urls.ts | 14 +
.../solid/src/routes/index.tsx | 18 +
.../server-fn-churn/solid/tsconfig.json | 17 +
.../server-fn-churn/solid/vite.config.ts | 34 ++
.../server-fn-churn/vue/memory.bench.ts | 10 +
.../server-fn-churn/vue/memory.flame.ts | 4 +
.../server-fn-churn/vue/project.json | 54 +++
.../scenarios/server-fn-churn/vue/setup.ts | 14 +
.../scenarios/server-fn-churn/vue/src/fns.ts | 47 +++
.../server-fn-churn/vue/src/routeTree.gen.ts | 86 +++++
.../server-fn-churn/vue/src/router.tsx | 16 +
.../server-fn-churn/vue/src/routes/__root.tsx | 29 ++
.../vue/src/routes/api.fn-urls.ts | 14 +
.../server-fn-churn/vue/src/routes/index.tsx | 18 +
.../server-fn-churn/vue/tsconfig.json | 17 +
.../server-fn-churn/vue/vite.config.ts | 29 ++
.../scenarios/streaming-peak/react/setup.ts | 138 +------
.../server/scenarios/streaming-peak/shared.ts | 146 ++++++++
.../streaming-peak/solid/memory.bench.ts | 10 +
.../streaming-peak/solid/memory.flame.ts | 4 +
.../streaming-peak/solid/project.json | 54 +++
.../scenarios/streaming-peak/solid/setup.ts | 14 +
.../streaming-peak/solid/src/routeTree.gen.ts | 86 +++++
.../streaming-peak/solid/src/router.tsx | 16 +
.../solid/src/routes/__root.tsx | 29 ++
.../streaming-peak/solid/src/routes/index.tsx | 9 +
.../solid/src/routes/stream.$id.tsx | 99 +++++
.../streaming-peak/solid/tsconfig.json | 17 +
.../streaming-peak/solid/vite.config.ts | 34 ++
.../streaming-peak/vue/memory.bench.ts | 10 +
.../streaming-peak/vue/memory.flame.ts | 4 +
.../scenarios/streaming-peak/vue/project.json | 54 +++
.../scenarios/streaming-peak/vue/setup.ts | 14 +
.../streaming-peak/vue/src/routeTree.gen.ts | 86 +++++
.../streaming-peak/vue/src/router.tsx | 16 +
.../streaming-peak/vue/src/routes/__root.tsx | 29 ++
.../streaming-peak/vue/src/routes/index.tsx | 9 +
.../vue/src/routes/stream.$id.tsx | 106 ++++++
.../streaming-peak/vue/tsconfig.json | 17 +
.../streaming-peak/vue/vite.config.ts | 29 ++
.../memory/server/vitest.solid.config.ts | 9 +
benchmarks/memory/server/vitest.vue.config.ts | 9 +
package.json | 6 +
.../src/ssr/renderRouterToStream.tsx | 42 ++-
.../tests/renderRouterToStream.test.tsx | 4 +-
pnpm-lock.yaml | 66 +++-
364 files changed, 12888 insertions(+), 1996 deletions(-)
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/shared.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/shared.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/project.json
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/project.json
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/shared.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/project.json
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/project.json
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/shared.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/project.json
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/project.json
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/shared.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json
create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts
create mode 100644 benchmarks/memory/client/vitest.solid.config.ts
create mode 100644 benchmarks/memory/client/vitest.vue.config.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/shared.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/project.json
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/project.json
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/shared.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/project.json
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/project.json
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/shared.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/project.json
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/project.json
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/shared.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/project.json
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/project.json
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/shared.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/project.json
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/project.json
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/shared.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/shared.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/project.json
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/project.json
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json
create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts
create mode 100644 benchmarks/memory/server/vitest.solid.config.ts
create mode 100644 benchmarks/memory/server/vitest.vue.config.ts
diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml
index 8ccb0d12cc..eae3cd7448 100644
--- a/.github/workflows/client-nav-benchmarks.yml
+++ b/.github/workflows/client-nav-benchmarks.yml
@@ -47,16 +47,6 @@ jobs:
mode: memory
- benchmark: memory-client
mode: memory
- # TODO: temp exclude memory benchmarks for solid and vue until we have them working
- exclude:
- - benchmark: memory-server
- framework: solid
- - benchmark: memory-server
- framework: vue
- - benchmark: memory-client
- framework: solid
- - benchmark: memory-client
- framework: vue
runs-on: ubuntu-latest
steps:
- name: Checkout
diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md
index 9cea416f0b..511acc6693 100644
--- a/benchmarks/memory/README.md
+++ b/benchmarks/memory/README.md
@@ -4,24 +4,25 @@ Dedicated memory benchmarks for TanStack Router / Start, measured with the
CodSpeed **memory instrument** (`mode: memory` in
`.github/workflows/client-nav-benchmarks.yml`). Two separate benchmarks:
-- `server/` (`@benchmarks/memory-server`) — React Start apps, requests against
+- `server/` (`@benchmarks/memory-server`) — React/Solid/Vue Start apps, requests against
the built server handler (`handler.fetch`), Node environment.
-- `client/` (`@benchmarks/memory-client`) — router-only React apps in jsdom.
+- `client/` (`@benchmarks/memory-client`) — router-only React/Solid/Vue apps in jsdom.
These deliberately do **not** reuse the CPU scenarios in `benchmarks/ssr` and
`benchmarks/client-nav`: memory benches need their own iteration counts,
payload sizes, and route shapes, and tuning those must never shift the CPU
-baselines. React-first; each scenario keeps a `react/` level so solid/vue can
-be added later without renames.
+baselines. Each scenario keeps a framework level (`react/`, `solid/`, `vue/`)
+so framework ports can be added without renames.
## Layout
```text
benchmarks/memory//
- package.json Nx targets: build:react, test:perf:react, test:flame:react, test:types
+ package.json Nx targets: build:, test:perf:, test:flame:, test:types
bench-utils.ts memoryBenchOptions, seeded LCG (+ sequential request loop on the server side)
- vitest.react.config.ts aggregates scenarios/*/react/vite.config.ts
- scenarios//react/ one isolated app per scenario + setup.ts + memory.bench.ts + memory.flame.ts
+ vitest..config.ts aggregates scenarios/*//vite.config.ts
+ scenarios///
+ one isolated app per scenario + setup.ts + memory.bench.ts + memory.flame.ts
```
One app per scenario; apps and bench names are stable once landed (CodSpeed
@@ -91,9 +92,8 @@ generation; `memory.bench.ts` and `memory.flame.ts` are thin runners only.
is fine — never overlap distinct work items.
- Randomness only via the seeded LCG in `bench-utils.ts`; no `Math.random`,
`Date.now`, or timers — with one documented exception: `streaming-peak`'s
- deferred sections use small `setTimeout` delays, because React schedules
- stream flushes via `setImmediate` and any non-timer deferral wins that race,
- suppressing the Suspense fallbacks the scenario exists to stream.
+ deferred sections use small `setTimeout` delays so deferred stream chunks are
+ observable across framework renderers.
- Sanity assertions run once at module load and throw on wrong
status/markers, so a bench can never silently measure the wrong thing.
- Server requests follow `benchmarks/ssr` conventions: document GETs send
@@ -101,7 +101,7 @@ generation; `memory.bench.ts` and `memory.flame.ts` are thin runners only.
with bodies precomputed at module level.
- Client apps export `mountTestApp` from `app.tsx`; benches import the built
`dist/app.js`; navigations use `replace: true`; unmount does full teardown
- (React root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads
+ (framework root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads
are never rendered into the DOM.
- `NODE_ENV=production` everywhere (the Nx targets set it).
@@ -112,7 +112,11 @@ scenarios:
```bash
pnpm nx run @benchmarks/memory-server:test:perf:react --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-server:test:perf:solid --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-server:test:perf:vue --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/memory-client:test:perf:react --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-client:test:perf:solid --outputStyle=stream --skipRemoteCache
+pnpm nx run @benchmarks/memory-client:test:perf:vue --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/memory-server:test:types --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemoteCache
```
@@ -130,6 +134,10 @@ inside `test:perf:react`.
```bash
pnpm benchmark:memory:server:flame
pnpm benchmark:memory:client:flame
+pnpm benchmark:memory:server:flame:solid
+pnpm benchmark:memory:client:flame:solid
+pnpm benchmark:memory:server:flame:vue
+pnpm benchmark:memory:client:flame:vue
```
To profile one scenario, run its `test:flame` target directly:
@@ -169,5 +177,9 @@ CodSpeed dashboard** — local runs do not affect PR baselines):
```bash
WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:react
+WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:solid
+WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:vue
WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:react
+WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:solid
+WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:vue
```
diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json
index 534225d2a3..d7fd463bde 100644
--- a/benchmarks/memory/client/package.json
+++ b/benchmarks/memory/client/package.json
@@ -22,8 +22,12 @@
"dependencies": {
"@tanstack/react-router": "workspace:*",
"@tanstack/router-core": "workspace:*",
+ "@tanstack/solid-router": "workspace:*",
+ "@tanstack/vue-router": "workspace:*",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "solid-js": "^1.9.10",
+ "vue": "^3.5.16"
},
"devDependencies": {
"@codspeed/vitest-plugin": "^5.5.0",
@@ -31,10 +35,13 @@
"@platformatic/flame": "^1.6.0",
"@testing-library/react": "^16.2.0",
"@vitejs/plugin-react": "^6.0.1",
+ "@vitejs/plugin-vue": "^6.0.5",
+ "@vitejs/plugin-vue-jsx": "^5.1.5",
"@types/jsdom": "28.0.0",
"jsdom": "29.1.1",
"typescript": "^6.0.2",
"vite": "^8.0.14",
+ "vite-plugin-solid": "^2.11.11",
"vitest": "^4.1.4"
},
"nx": {
@@ -56,6 +63,40 @@
}
]
},
+ "build:solid": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-solid",
+ "@benchmarks/memory-client-unique-location-churn-solid",
+ "@benchmarks/memory-client-preload-churn-solid",
+ "@benchmarks/memory-client-loader-data-retention-solid",
+ "@benchmarks/memory-client-mount-unmount-solid",
+ "@benchmarks/memory-client-interrupted-navigations-solid"
+ ],
+ "target": "build:client"
+ }
+ ]
+ },
+ "build:vue": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-vue",
+ "@benchmarks/memory-client-unique-location-churn-vue",
+ "@benchmarks/memory-client-preload-churn-vue",
+ "@benchmarks/memory-client-loader-data-retention-vue",
+ "@benchmarks/memory-client-mount-unmount-vue",
+ "@benchmarks/memory-client-interrupted-navigations-vue"
+ ],
+ "target": "build:client"
+ }
+ ]
+ },
"build:react:flame": {
"executor": "nx:noop",
"cache": false,
@@ -73,6 +114,40 @@
}
]
},
+ "build:solid:flame": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-solid",
+ "@benchmarks/memory-client-unique-location-churn-solid",
+ "@benchmarks/memory-client-preload-churn-solid",
+ "@benchmarks/memory-client-loader-data-retention-solid",
+ "@benchmarks/memory-client-mount-unmount-solid",
+ "@benchmarks/memory-client-interrupted-navigations-solid"
+ ],
+ "target": "build:client:flame"
+ }
+ ]
+ },
+ "build:vue:flame": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-vue",
+ "@benchmarks/memory-client-unique-location-churn-vue",
+ "@benchmarks/memory-client-preload-churn-vue",
+ "@benchmarks/memory-client-loader-data-retention-vue",
+ "@benchmarks/memory-client-mount-unmount-vue",
+ "@benchmarks/memory-client-interrupted-navigations-vue"
+ ],
+ "target": "build:client:flame"
+ }
+ ]
+ },
"test:flame:react": {
"executor": "nx:noop",
"cache": false,
@@ -90,6 +165,40 @@
}
]
},
+ "test:flame:solid": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-solid",
+ "@benchmarks/memory-client-unique-location-churn-solid",
+ "@benchmarks/memory-client-preload-churn-solid",
+ "@benchmarks/memory-client-loader-data-retention-solid",
+ "@benchmarks/memory-client-mount-unmount-solid",
+ "@benchmarks/memory-client-interrupted-navigations-solid"
+ ],
+ "target": "test:flame"
+ }
+ ]
+ },
+ "test:flame:vue": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-client-navigation-churn-vue",
+ "@benchmarks/memory-client-unique-location-churn-vue",
+ "@benchmarks/memory-client-preload-churn-vue",
+ "@benchmarks/memory-client-loader-data-retention-vue",
+ "@benchmarks/memory-client-mount-unmount-vue",
+ "@benchmarks/memory-client-interrupted-navigations-vue"
+ ],
+ "target": "test:flame"
+ }
+ ]
+ },
"test:perf:react": {
"executor": "nx:run-commands",
"parallelism": false,
@@ -102,6 +211,30 @@
"cwd": "benchmarks/memory/client"
}
},
+ "test:perf:solid": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": [
+ "build:solid"
+ ],
+ "options": {
+ "command": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts",
+ "cwd": "benchmarks/memory/client"
+ }
+ },
+ "test:perf:vue": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": [
+ "build:vue"
+ ],
+ "options": {
+ "command": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts",
+ "cwd": "benchmarks/memory/client"
+ }
+ },
"test:types": {
"executor": "nx:noop",
"dependsOn": [
@@ -112,7 +245,19 @@
"@benchmarks/memory-client-preload-churn-react",
"@benchmarks/memory-client-loader-data-retention-react",
"@benchmarks/memory-client-mount-unmount-react",
- "@benchmarks/memory-client-interrupted-navigations-react"
+ "@benchmarks/memory-client-interrupted-navigations-react",
+ "@benchmarks/memory-client-navigation-churn-solid",
+ "@benchmarks/memory-client-unique-location-churn-solid",
+ "@benchmarks/memory-client-preload-churn-solid",
+ "@benchmarks/memory-client-loader-data-retention-solid",
+ "@benchmarks/memory-client-mount-unmount-solid",
+ "@benchmarks/memory-client-interrupted-navigations-solid",
+ "@benchmarks/memory-client-navigation-churn-vue",
+ "@benchmarks/memory-client-unique-location-churn-vue",
+ "@benchmarks/memory-client-preload-churn-vue",
+ "@benchmarks/memory-client-loader-data-retention-vue",
+ "@benchmarks/memory-client-mount-unmount-vue",
+ "@benchmarks/memory-client-interrupted-navigations-vue"
],
"target": "test:types:client"
}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
index d0532e87e6..5a61a00e61 100644
--- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts
@@ -1,18 +1,5 @@
import type * as App from './src/app'
-import {
- createDeterministicRandom,
- randomSegment,
-} from '#memory-client/bench-utils'
-
-type NavigationSettlement =
- | {
- status: 'fulfilled'
- value: void
- }
- | {
- status: 'rejected'
- reason: unknown
- }
+import { createSetup } from '../shared'
const appModulePath = './dist/app.js'
const {
@@ -21,275 +8,13 @@ const {
resolveSlowLoader,
slowLoaderRegistry,
} = (await import(/* @vite-ignore */ appModulePath)) as typeof App
-const interruptedNavigationIterations = 150
-const interruptedNavigationPairs = createInterruptedNavigationPairs(
- interruptedNavigationIterations,
-)
-
-const uninitialized = () =>
- Promise.reject(
- new Error('interrupted-navigations benchmark is not initialized'),
- )
-
-const uninitializedSettlement = () =>
- Promise.resolve({
- status: 'rejected',
- reason: new Error('interrupted-navigations benchmark is not initialized'),
- })
-
-function createInterruptedNavigationPairs(iterations: number) {
- const random = createDeterministicRandom(13)
-
- return Array.from({ length: iterations }, (_, index) => ({
- slowId: `slow-${index}-${randomSegment(random)}`,
- fastId: `fast-${index}-${randomSegment(random)}`,
- }))
-}
-
-async function drainMicrotasks() {
- await Promise.resolve()
- await Promise.resolve()
-}
-
-function formatReason(reason: unknown) {
- if (reason instanceof Error) {
- return `${reason.name}: ${reason.message}`
- }
-
- return String(reason)
-}
-
-function assertSlowNavigationSettlement(settlement: NavigationSettlement) {
- if (settlement.status === 'fulfilled') {
- if (settlement.value !== undefined) {
- throw new Error('Expected slow navigation to fulfill with undefined')
- }
-
- return
- }
-
- if (
- reasonHasAbortShape(settlement.reason) ||
- reasonHasCancellationShape(settlement.reason)
- ) {
- return
- }
-
- throw new Error(
- `Expected slow navigation to settle as void or cancellation, got ${formatReason(
- settlement.reason,
- )}`,
- )
-}
-
-async function awaitExpectedLoadSettlement(loadPromise: Promise) {
- try {
- await loadPromise
- } catch (reason) {
- if (reasonHasAbortShape(reason) || reasonHasCancellationShape(reason)) {
- return
- }
-
- throw reason
- }
-}
-
-function reasonHasAbortShape(reason: unknown) {
- return reason instanceof DOMException && reason.name === 'AbortError'
-}
-
-function reasonHasCancellationShape(reason: unknown) {
- return (
- reason instanceof Error &&
- (reason.name === 'AbortError' || reason.name === 'CancelledError')
- )
-}
export function setup() {
- if (process.env.NODE_ENV !== 'production') {
- console.warn(
- 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
- )
- }
-
- let container: HTMLDivElement | undefined = undefined
- let unmount: (() => void) | undefined = undefined
- let unsub = () => {}
- let resolveRendered: () => void = () => {}
- let expectedRenderedPath: string | undefined = undefined
- let navigateFast: (id: string) => Promise = uninitialized
- let startSlowNavigation: (id: string) => Promise =
- uninitializedSettlement
- let getLatestLoadPromise: () => Promise | undefined = () => undefined
-
- function assertRenderedPage(page: 'shell' | 'fast', id?: string) {
- const element = container?.querySelector('[data-bench-page]')
- const actualPage = element?.dataset.benchPage
- const actualId = element?.dataset.benchId
-
- if (actualPage !== page) {
- throw new Error(`Expected rendered page ${page}, got ${actualPage}`)
- }
-
- if (id !== undefined && actualId !== id) {
- throw new Error(`Expected rendered id ${id}, got ${actualId}`)
- }
- }
-
- async function waitForRenderedPage(page: 'shell' | 'fast', id?: string) {
- for (let attempt = 0; attempt < 10; attempt++) {
- try {
- assertRenderedPage(page, id)
- return
- } catch {
- await new Promise((resolve) => {
- requestAnimationFrame(() => resolve())
- })
- }
- }
-
- assertRenderedPage(page, id)
- }
-
- async function waitForSlowLoader(id: string) {
- for (let attempt = 0; attempt < 20; attempt++) {
- if (slowLoaderRegistry.has(id)) {
- return
- }
-
- await drainMicrotasks()
- }
-
- throw new Error(`Slow loader was not registered for id: ${id}`)
- }
-
- function waitForNextRender(pathname: string) {
- expectedRenderedPath = pathname
-
- return new Promise((resolve) => {
- resolveRendered = resolve
- })
- }
-
- async function before() {
- if (container) {
- after()
- }
-
- container = document.createElement('div')
- document.body.append(container)
-
- const mounted = mountTestApp(container)
- const { router } = mounted
- unmount = mounted.unmount
- getLatestLoadPromise = () => router.latestLoadPromise
-
- unsub = router.subscribe('onRendered', (event) => {
- if (
- expectedRenderedPath &&
- event.toLocation.pathname !== expectedRenderedPath
- ) {
- return
- }
-
- const resolve = resolveRendered
- resolveRendered = () => {}
- expectedRenderedPath = undefined
- resolve()
- })
-
- navigateFast = async (id) => {
- const rendered = waitForNextRender(`/fast/${id}`)
- await Promise.all([
- router.navigate({
- to: '/fast/$id',
- params: { id },
- replace: true,
- }),
- rendered,
- ])
- }
-
- startSlowNavigation = (id) => {
- const navigation = router.navigate({
- to: '/slow/$id',
- params: { id },
- replace: true,
- })
-
- return navigation
- .then((value): NavigationSettlement => ({ status: 'fulfilled', value }))
- .catch(
- (reason): NavigationSettlement => ({ status: 'rejected', reason }),
- )
- }
-
- await router.load()
- await waitForRenderedPage('shell')
- }
-
- function after() {
- resolveAllSlowLoaders()
- unmount?.()
- container?.remove()
- unsub()
-
- container = undefined
- unmount = undefined
- unsub = () => {}
- resolveRendered = () => {}
- expectedRenderedPath = undefined
- navigateFast = uninitialized
- startSlowNavigation = uninitializedSettlement
- getLatestLoadPromise = () => undefined
- }
-
- async function interrupt(
- slowId: string,
- fastId: string,
- assertShape = false,
- ) {
- const slowNavigation = startSlowNavigation(slowId)
-
- await waitForSlowLoader(slowId)
- const slowLoadPromise = getLatestLoadPromise()
-
- if (!slowLoadPromise) {
- throw new Error(`Slow navigation did not start a load for id: ${slowId}`)
- }
-
- await navigateFast(fastId)
- resolveSlowLoader(slowId)
-
- const settlement = await slowNavigation
- assertSlowNavigationSettlement(settlement)
- await awaitExpectedLoadSettlement(slowLoadPromise)
- await drainMicrotasks()
-
- if (assertShape) {
- assertRenderedPage('fast', fastId)
- }
- }
-
- return {
- name: 'mem interrupted-navigations (react)',
- before,
- interrupt,
- async run() {
- for (const pair of interruptedNavigationPairs) {
- await interrupt(pair.slowId, pair.fastId)
- }
- },
- async sanity() {
- await before()
-
- try {
- assertRenderedPage('shell')
- await interrupt('sanity-slow', 'sanity-fast', true)
- } finally {
- after()
- }
- },
- after,
- }
+ return createSetup(
+ 'react',
+ mountTestApp,
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+ )
}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts
new file mode 100644
index 0000000000..ede8ad2bc0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts
@@ -0,0 +1,339 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
+
+type Framework = 'react' | 'solid' | 'vue'
+
+type NavigationSettlement =
+ | {
+ status: 'fulfilled'
+ value: void
+ }
+ | {
+ status: 'rejected'
+ reason: unknown
+ }
+
+type MountedApp = {
+ router: unknown
+ unmount: () => void
+}
+
+type MountTestApp = (container: HTMLDivElement) => MountedApp
+type ResolveAllSlowLoaders = () => void
+type ResolveSlowLoader = (id: string) => void
+type SlowLoaderRegistry = {
+ has: (id: string) => boolean
+}
+
+type RenderedEvent = {
+ toLocation: {
+ pathname: string
+ }
+}
+
+type InterruptedNavigationRouter = {
+ latestLoadPromise: Promise | undefined
+ load: () => Promise
+ navigate: (options: {
+ to: '/fast/$id' | '/slow/$id'
+ params: { id: string }
+ replace: true
+ }) => Promise
+ subscribe: (
+ event: 'onRendered',
+ listener: (event: RenderedEvent) => void,
+ ) => () => void
+}
+
+const frameworkNames = {
+ react: 'React',
+ solid: 'Solid',
+ vue: 'Vue',
+} satisfies Record
+const interruptedNavigationIterations = 150
+const interruptedNavigationPairs = createInterruptedNavigationPairs(
+ interruptedNavigationIterations,
+)
+
+const uninitialized = () =>
+ Promise.reject(
+ new Error('interrupted-navigations benchmark is not initialized'),
+ )
+
+const uninitializedSettlement = () =>
+ Promise.resolve({
+ status: 'rejected',
+ reason: new Error('interrupted-navigations benchmark is not initialized'),
+ })
+
+function warnDevMode(framework: Framework) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`,
+ )
+ }
+}
+
+function createInterruptedNavigationPairs(iterations: number) {
+ const random = createDeterministicRandom(13)
+
+ return Array.from({ length: iterations }, (_, index) => ({
+ slowId: `slow-${index}-${randomSegment(random)}`,
+ fastId: `fast-${index}-${randomSegment(random)}`,
+ }))
+}
+
+async function drainMicrotasks() {
+ await Promise.resolve()
+ await Promise.resolve()
+}
+
+function formatReason(reason: unknown) {
+ if (reason instanceof Error) {
+ return `${reason.name}: ${reason.message}`
+ }
+
+ return String(reason)
+}
+
+function assertSlowNavigationSettlement(settlement: NavigationSettlement) {
+ if (settlement.status === 'fulfilled') {
+ if (settlement.value !== undefined) {
+ throw new Error('Expected slow navigation to fulfill with undefined')
+ }
+
+ return
+ }
+
+ if (
+ reasonHasAbortShape(settlement.reason) ||
+ reasonHasCancellationShape(settlement.reason)
+ ) {
+ return
+ }
+
+ throw new Error(
+ `Expected slow navigation to settle as void or cancellation, got ${formatReason(
+ settlement.reason,
+ )}`,
+ )
+}
+
+async function awaitExpectedLoadSettlement(loadPromise: Promise) {
+ try {
+ await loadPromise
+ } catch (reason) {
+ if (reasonHasAbortShape(reason) || reasonHasCancellationShape(reason)) {
+ return
+ }
+
+ throw reason
+ }
+}
+
+function reasonHasAbortShape(reason: unknown) {
+ return reason instanceof DOMException && reason.name === 'AbortError'
+}
+
+function reasonHasCancellationShape(reason: unknown) {
+ return (
+ reason instanceof Error &&
+ (reason.name === 'AbortError' || reason.name === 'CancelledError')
+ )
+}
+
+export function createSetup(
+ framework: Framework,
+ mountTestApp: MountTestApp,
+ resolveAllSlowLoaders: ResolveAllSlowLoaders,
+ resolveSlowLoader: ResolveSlowLoader,
+ slowLoaderRegistry: SlowLoaderRegistry,
+) {
+ warnDevMode(framework)
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let expectedRenderedPath: string | undefined = undefined
+ let navigateFast: (id: string) => Promise = uninitialized
+ let startSlowNavigation: (id: string) => Promise =
+ uninitializedSettlement
+ let getLatestLoadPromise: () => Promise | undefined = () => undefined
+
+ function assertRenderedPage(page: 'shell' | 'fast', id?: string) {
+ const element = container?.querySelector('[data-bench-page]')
+ const actualPage = element?.dataset.benchPage
+ const actualId = element?.dataset.benchId
+
+ if (actualPage !== page) {
+ throw new Error(`Expected rendered page ${page}, got ${actualPage}`)
+ }
+
+ if (id !== undefined && actualId !== id) {
+ throw new Error(`Expected rendered id ${id}, got ${actualId}`)
+ }
+ }
+
+ async function waitForRenderedPage(page: 'shell' | 'fast', id?: string) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedPage(page, id)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedPage(page, id)
+ }
+
+ async function waitForSlowLoader(id: string) {
+ for (let attempt = 0; attempt < 20; attempt++) {
+ if (slowLoaderRegistry.has(id)) {
+ return
+ }
+
+ await drainMicrotasks()
+ }
+
+ throw new Error(`Slow loader was not registered for id: ${id}`)
+ }
+
+ function waitForNextRender(pathname: string) {
+ expectedRenderedPath = pathname
+
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const router = mounted.router as InterruptedNavigationRouter
+ unmount = mounted.unmount
+ getLatestLoadPromise = () => router.latestLoadPromise
+
+ unsub = router.subscribe('onRendered', (event) => {
+ if (
+ expectedRenderedPath &&
+ event.toLocation.pathname !== expectedRenderedPath
+ ) {
+ return
+ }
+
+ const resolve = resolveRendered
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ resolve()
+ })
+
+ navigateFast = async (id) => {
+ const rendered = waitForNextRender(`/fast/${id}`)
+ await Promise.all([
+ router.navigate({
+ to: '/fast/$id',
+ params: { id },
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ startSlowNavigation = (id) => {
+ const navigation = router.navigate({
+ to: '/slow/$id',
+ params: { id },
+ replace: true,
+ })
+
+ return navigation
+ .then((value): NavigationSettlement => ({ status: 'fulfilled', value }))
+ .catch(
+ (reason: unknown): NavigationSettlement => ({
+ status: 'rejected',
+ reason,
+ }),
+ )
+ }
+
+ await router.load()
+ await waitForRenderedPage('shell')
+ }
+
+ function after() {
+ resolveAllSlowLoaders()
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ navigateFast = uninitialized
+ startSlowNavigation = uninitializedSettlement
+ getLatestLoadPromise = () => undefined
+ }
+
+ async function interrupt(
+ slowId: string,
+ fastId: string,
+ assertShape = false,
+ ) {
+ const slowNavigation = startSlowNavigation(slowId)
+
+ await waitForSlowLoader(slowId)
+ const slowLoadPromise = getLatestLoadPromise()
+
+ if (!slowLoadPromise) {
+ throw new Error(`Slow navigation did not start a load for id: ${slowId}`)
+ }
+
+ await navigateFast(fastId)
+ resolveSlowLoader(slowId)
+
+ const settlement = await slowNavigation
+ assertSlowNavigationSettlement(settlement)
+ await awaitExpectedLoadSettlement(slowLoadPromise)
+ await drainMicrotasks()
+
+ if (assertShape) {
+ assertRenderedPage('fast', fastId)
+ }
+ }
+
+ return {
+ name: `mem interrupted-navigations (${framework})`,
+ before,
+ interrupt,
+ async run() {
+ for (const pair of interruptedNavigationPairs) {
+ await interrupt(pair.slowId, pair.fastId)
+ }
+ },
+ async sanity() {
+ await before()
+
+ try {
+ assertRenderedPage('shell')
+ await interrupt('sanity-slow', 'sanity-fast', true)
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json
new file mode 100644
index 0000000000..4aff39500e
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-interrupted-navigations-solid",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts
new file mode 100644
index 0000000000..28deb89580
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts
@@ -0,0 +1,20 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const {
+ mountTestApp,
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+} = (await import(/* @vite-ignore */ appModulePath)) as typeof App
+
+export function setup() {
+ return createSetup(
+ 'solid',
+ mountTestApp,
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx
new file mode 100644
index 0000000000..47aa9d8a37
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx
@@ -0,0 +1,35 @@
+import { RouterProvider } from '@tanstack/solid-router'
+import { render } from 'solid-js/web'
+import { getRouter } from './router'
+
+export {
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+} from './slow-loaders'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const dispose = render(() => , container)
+ let didUnmount = false
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ dispose()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..f42255963c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts
@@ -0,0 +1,95 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as SlowIdRouteImport } from './routes/slow.$id'
+import { Route as FastIdRouteImport } from './routes/fast.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const SlowIdRoute = SlowIdRouteImport.update({
+ id: '/slow/$id',
+ path: '/slow/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FastIdRoute = FastIdRouteImport.update({
+ id: '/fast/$id',
+ path: '/fast/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/fast/$id' | '/slow/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/fast/$id' | '/slow/$id'
+ id: '__root__' | '/' | '/fast/$id' | '/slow/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ FastIdRoute: typeof FastIdRoute
+ SlowIdRoute: typeof SlowIdRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/slow/$id': {
+ id: '/slow/$id'
+ path: '/slow/$id'
+ fullPath: '/slow/$id'
+ preLoaderRoute: typeof SlowIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/fast/$id': {
+ id: '/fast/$id'
+ path: '/fast/$id'
+ fullPath: '/fast/$id'
+ preLoaderRoute: typeof FastIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ FastIdRoute: FastIdRoute,
+ SlowIdRoute: SlowIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx
new file mode 100644
index 0000000000..884aa3438f
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..cb8d5a688d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx
new file mode 100644
index 0000000000..d1871d287b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { fixedTimestamp } from '../slow-loaders'
+
+export const Route = createFileRoute('/fast/$id')({
+ loader: ({ params }) => ({
+ id: params.id,
+ kind: 'fast' as const,
+ ts: fixedTimestamp,
+ }),
+ component: FastComponent,
+})
+
+function FastComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data().kind}:${data().id}:${data().ts}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx
new file mode 100644
index 0000000000..592b2d666e
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return shell
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx
new file mode 100644
index 0000000000..1e645569b7
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { getSlowLoaderDeferred } from '../slow-loaders'
+
+export const Route = createFileRoute('/slow/$id')({
+ loader: async ({ params }) => {
+ const deferred = getSlowLoaderDeferred(params.id)
+
+ return await deferred.promise
+ },
+ component: SlowComponent,
+})
+
+function SlowComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data().kind}:${data().id}:${data().ts}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts
new file mode 100644
index 0000000000..c70ccac3da
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts
@@ -0,0 +1,54 @@
+export const fixedTimestamp = 1_700_000_000_000
+
+type SlowLoaderPayload = {
+ id: string
+ kind: 'slow'
+ ts: number
+}
+
+type SlowLoaderDeferred = {
+ promise: Promise
+ resolve: () => void
+}
+
+export const slowLoaderRegistry = new Map()
+
+export function getSlowLoaderDeferred(id: string) {
+ const existing = slowLoaderRegistry.get(id)
+
+ if (existing) {
+ return existing
+ }
+
+ let resolvePromise!: (payload: SlowLoaderPayload) => void
+ const promise = new Promise((resolve) => {
+ resolvePromise = resolve
+ })
+
+ const deferred: SlowLoaderDeferred = {
+ promise,
+ resolve() {
+ slowLoaderRegistry.delete(id)
+ resolvePromise({ id, kind: 'slow', ts: fixedTimestamp })
+ },
+ }
+
+ slowLoaderRegistry.set(id, deferred)
+ return deferred
+}
+
+export function resolveSlowLoader(id: string) {
+ const deferred = slowLoaderRegistry.get(id)
+
+ if (!deferred) {
+ throw new Error(`No pending slow loader for id: ${id}`)
+ }
+
+ deferred.resolve()
+}
+
+export function resolveAllSlowLoaders() {
+ for (const id of Array.from(slowLoaderRegistry.keys())) {
+ resolveSlowLoader(id)
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json
new file mode 100644
index 0000000000..b12dcb7ade
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts
new file mode 100644
index 0000000000..e710e0fd12
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ solid({ hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client interrupted-navigations (solid)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json
new file mode 100644
index 0000000000..01288cb235
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-interrupted-navigations-vue",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts
new file mode 100644
index 0000000000..fad57cfc8c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts
@@ -0,0 +1,20 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const {
+ mountTestApp,
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+} = (await import(/* @vite-ignore */ appModulePath)) as typeof App
+
+export function setup() {
+ return createSetup(
+ 'vue',
+ mountTestApp,
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx
new file mode 100644
index 0000000000..4257601dbd
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx
@@ -0,0 +1,42 @@
+import { RouterProvider } from '@tanstack/vue-router'
+import { createApp } from 'vue'
+import { getRouter } from './router'
+import type {} from '@tanstack/router-core'
+
+export {
+ resolveAllSlowLoaders,
+ resolveSlowLoader,
+ slowLoaderRegistry,
+} from './slow-loaders'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const vueApp = createApp({
+ setup() {
+ return () =>
+ },
+ })
+ let didUnmount = false
+
+ vueApp.mount(container)
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ vueApp.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..5ea61a89a3
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts
@@ -0,0 +1,95 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as SlowIdRouteImport } from './routes/slow.$id'
+import { Route as FastIdRouteImport } from './routes/fast.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const SlowIdRoute = SlowIdRouteImport.update({
+ id: '/slow/$id',
+ path: '/slow/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FastIdRoute = FastIdRouteImport.update({
+ id: '/fast/$id',
+ path: '/fast/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/fast/$id': typeof FastIdRoute
+ '/slow/$id': typeof SlowIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/fast/$id' | '/slow/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/fast/$id' | '/slow/$id'
+ id: '__root__' | '/' | '/fast/$id' | '/slow/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ FastIdRoute: typeof FastIdRoute
+ SlowIdRoute: typeof SlowIdRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/slow/$id': {
+ id: '/slow/$id'
+ path: '/slow/$id'
+ fullPath: '/slow/$id'
+ preLoaderRoute: typeof SlowIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/fast/$id': {
+ id: '/fast/$id'
+ path: '/fast/$id'
+ fullPath: '/fast/$id'
+ preLoaderRoute: typeof FastIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ FastIdRoute: FastIdRoute,
+ SlowIdRoute: SlowIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx
new file mode 100644
index 0000000000..4655fe8424
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..91296e6f84
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx
new file mode 100644
index 0000000000..ff5e828909
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { fixedTimestamp } from '../slow-loaders'
+
+export const Route = createFileRoute('/fast/$id')({
+ loader: ({ params }: { params: { id: string } }) => ({
+ id: params.id,
+ kind: 'fast' as const,
+ ts: fixedTimestamp,
+ }),
+ component: FastComponent,
+})
+
+function FastComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.value.kind}:${data.value.id}:${data.value.ts}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx
new file mode 100644
index 0000000000..c30a51e1e5
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return shell
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx
new file mode 100644
index 0000000000..5ac5b44d44
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { getSlowLoaderDeferred } from '../slow-loaders'
+
+export const Route = createFileRoute('/slow/$id')({
+ loader: async ({ params }: { params: { id: string } }) => {
+ const deferred = getSlowLoaderDeferred(params.id)
+
+ return await deferred.promise
+ },
+ component: SlowComponent,
+})
+
+function SlowComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.value.kind}:${data.value.id}:${data.value.ts}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts
new file mode 100644
index 0000000000..c70ccac3da
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts
@@ -0,0 +1,54 @@
+export const fixedTimestamp = 1_700_000_000_000
+
+type SlowLoaderPayload = {
+ id: string
+ kind: 'slow'
+ ts: number
+}
+
+type SlowLoaderDeferred = {
+ promise: Promise
+ resolve: () => void
+}
+
+export const slowLoaderRegistry = new Map()
+
+export function getSlowLoaderDeferred(id: string) {
+ const existing = slowLoaderRegistry.get(id)
+
+ if (existing) {
+ return existing
+ }
+
+ let resolvePromise!: (payload: SlowLoaderPayload) => void
+ const promise = new Promise((resolve) => {
+ resolvePromise = resolve
+ })
+
+ const deferred: SlowLoaderDeferred = {
+ promise,
+ resolve() {
+ slowLoaderRegistry.delete(id)
+ resolvePromise({ id, kind: 'slow', ts: fixedTimestamp })
+ },
+ }
+
+ slowLoaderRegistry.set(id, deferred)
+ return deferred
+}
+
+export function resolveSlowLoader(id: string) {
+ const deferred = slowLoaderRegistry.get(id)
+
+ if (!deferred) {
+ throw new Error(`No pending slow loader for id: ${id}`)
+ }
+
+ deferred.resolve()
+}
+
+export function resolveAllSlowLoaders() {
+ for (const id of Array.from(slowLoaderRegistry.keys())) {
+ resolveSlowLoader(id)
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json
new file mode 100644
index 0000000000..9a5872a4c0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts
new file mode 100644
index 0000000000..b2912ce9d1
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts
@@ -0,0 +1,36 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ vue(),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client interrupted-navigations (vue)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
index 934f37c1bf..1f84970d14 100644
--- a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts
@@ -1,169 +1,11 @@
import type * as App from './src/app'
-import {
- createDeterministicRandom,
- randomSegment,
-} from '#memory-client/bench-utils'
+import { createSetup } from '../shared'
const appModulePath = './dist/app.js'
const { loaderPayloadRecordCount, mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
-const loaderDataRetentionNavigationCount = 20
-const pageIds = createPageIds()
-
-const uninitialized = () =>
- Promise.reject(
- new Error('loader-data-retention benchmark is not initialized'),
- )
-
-function createPageIds() {
- const random = createDeterministicRandom(11)
-
- return Array.from(
- { length: loaderDataRetentionNavigationCount },
- (_, index) => `${index}-${randomSegment(random)}`,
- )
-}
export function setup() {
- if (process.env.NODE_ENV !== 'production') {
- console.warn(
- 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
- )
- }
-
- let container: HTMLDivElement | undefined = undefined
- let unmount: (() => void) | undefined = undefined
- let unsub = () => {}
- let resolveRendered: () => void = () => {}
- let expectedRenderedPath: string | undefined = undefined
- let navigateTo: (id: string) => Promise = uninitialized
-
- function assertRenderedShell() {
- const actual =
- container?.querySelector('[data-bench-page]')?.dataset
- .benchPage
-
- if (actual !== 'shell') {
- throw new Error(`Expected rendered shell page, got ${actual}`)
- }
- }
-
- function assertRenderedPage(id: string) {
- const page = container?.querySelector(
- '[data-bench-page="page"]',
- )
- const actualId = page?.dataset.benchId
- const actualCount = page?.dataset.benchCount
- const expectedCount = String(loaderPayloadRecordCount)
-
- if (actualId !== id || actualCount !== expectedCount) {
- throw new Error(
- `Expected rendered page ${id}:${expectedCount}, got ${actualId}:${actualCount}`,
- )
- }
- }
-
- async function waitForRenderedShell() {
- for (let attempt = 0; attempt < 10; attempt++) {
- try {
- assertRenderedShell()
- return
- } catch {
- await new Promise((resolve) => {
- requestAnimationFrame(() => resolve())
- })
- }
- }
-
- assertRenderedShell()
- }
-
- function waitForNextRender(pathname: string) {
- expectedRenderedPath = pathname
-
- return new Promise((resolve) => {
- resolveRendered = resolve
- })
- }
-
- async function before() {
- if (container) {
- after()
- }
-
- container = document.createElement('div')
- document.body.append(container)
-
- const mounted = mountTestApp(container)
- const { router } = mounted
- unmount = mounted.unmount
-
- unsub = router.subscribe('onRendered', (event) => {
- if (
- expectedRenderedPath &&
- event.toLocation.pathname !== expectedRenderedPath
- ) {
- return
- }
-
- const resolve = resolveRendered
- resolveRendered = () => {}
- expectedRenderedPath = undefined
- resolve()
- })
-
- navigateTo = async (id) => {
- const pathname = `/page/${id}`
- const rendered = waitForNextRender(pathname)
-
- await router.navigate({
- to: '/page/$id',
- params: { id },
- replace: true,
- })
- await rendered
- assertRenderedPage(id)
- }
-
- await router.load()
- await waitForRenderedShell()
- }
-
- function after() {
- unmount?.()
- container?.remove()
- unsub()
-
- container = undefined
- unmount = undefined
- unsub = () => {}
- resolveRendered = () => {}
- expectedRenderedPath = undefined
- navigateTo = uninitialized
- }
-
- return {
- name: 'mem loader-data-retention (react)',
- before,
- navigate: (id: string) => navigateTo(id),
- async run() {
- for (const id of pageIds) {
- await navigateTo(id)
- }
- },
- async sanity() {
- await before()
-
- try {
- await navigateTo('sanity-a')
- assertRenderedPage('sanity-a')
- await navigateTo('sanity-b')
- assertRenderedPage('sanity-b')
- } finally {
- after()
- }
- },
- after,
- }
+ return createSetup('react', mountTestApp, loaderPayloadRecordCount)
}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts
new file mode 100644
index 0000000000..06c8f6eecd
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts
@@ -0,0 +1,205 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
+
+type Framework = 'react' | 'solid' | 'vue'
+
+type MountedApp = {
+ router: unknown
+ unmount: () => void
+}
+
+type MountTestApp = (container: HTMLDivElement) => MountedApp
+
+type RenderEvent = {
+ toLocation: {
+ pathname: string
+ }
+}
+
+type LoaderDataRouter = {
+ load: () => Promise
+ navigate: (options: {
+ to: '/page/$id'
+ params: { id: string }
+ replace: true
+ }) => Promise
+ subscribe: (
+ event: 'onRendered',
+ listener: (event: RenderEvent) => void,
+ ) => () => void
+}
+
+const frameworkNames = {
+ react: 'React',
+ solid: 'Solid',
+ vue: 'Vue',
+} satisfies Record
+const loaderDataRetentionNavigationCount = 20
+const pageIds = createPageIds()
+
+const uninitialized = () =>
+ Promise.reject(
+ new Error('loader-data-retention benchmark is not initialized'),
+ )
+
+function warnDevMode(framework: Framework) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`,
+ )
+ }
+}
+
+function createPageIds() {
+ const random = createDeterministicRandom(11)
+
+ return Array.from(
+ { length: loaderDataRetentionNavigationCount },
+ (_, index) => `${index}-${randomSegment(random)}`,
+ )
+}
+
+export function createSetup(
+ framework: Framework,
+ mountTestApp: MountTestApp,
+ loaderPayloadRecordCount: number,
+) {
+ warnDevMode(framework)
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let expectedRenderedPath: string | undefined = undefined
+ let navigateTo: (id: string) => Promise = uninitialized
+
+ function assertRenderedShell() {
+ const actual =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+
+ if (actual !== 'shell') {
+ throw new Error(`Expected rendered shell page, got ${actual}`)
+ }
+ }
+
+ function assertRenderedPage(id: string) {
+ const page = container?.querySelector(
+ '[data-bench-page="page"]',
+ )
+ const actualId = page?.dataset.benchId
+ const actualCount = page?.dataset.benchCount
+ const expectedCount = String(loaderPayloadRecordCount)
+
+ if (actualId !== id || actualCount !== expectedCount) {
+ throw new Error(
+ `Expected rendered page ${id}:${expectedCount}, got ${actualId}:${actualCount}`,
+ )
+ }
+ }
+
+ async function waitForRenderedShell() {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedShell()
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedShell()
+ }
+
+ function waitForNextRender(pathname: string) {
+ expectedRenderedPath = pathname
+
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const router = mounted.router as LoaderDataRouter
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', (event) => {
+ if (
+ expectedRenderedPath &&
+ event.toLocation.pathname !== expectedRenderedPath
+ ) {
+ return
+ }
+
+ const resolve = resolveRendered
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ resolve()
+ })
+
+ navigateTo = async (id) => {
+ const pathname = `/page/${id}`
+ const rendered = waitForNextRender(pathname)
+
+ await router.navigate({
+ to: '/page/$id',
+ params: { id },
+ replace: true,
+ })
+ await rendered
+ assertRenderedPage(id)
+ }
+
+ await router.load()
+ await waitForRenderedShell()
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ expectedRenderedPath = undefined
+ navigateTo = uninitialized
+ }
+
+ return {
+ name: `mem loader-data-retention (${framework})`,
+ before,
+ navigate: (id: string) => navigateTo(id),
+ async run() {
+ for (const id of pageIds) {
+ await navigateTo(id)
+ }
+ },
+ async sanity() {
+ await before()
+
+ try {
+ await navigateTo('sanity-a')
+ assertRenderedPage('sanity-a')
+ await navigateTo('sanity-b')
+ assertRenderedPage('sanity-b')
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json
new file mode 100644
index 0000000000..6c85f51896
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-loader-data-retention-solid",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts
new file mode 100644
index 0000000000..56a22a4ac5
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { loaderPayloadRecordCount, mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('solid', mountTestApp, loaderPayloadRecordCount)
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx
new file mode 100644
index 0000000000..453f50f330
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx
@@ -0,0 +1,31 @@
+import { RouterProvider } from '@tanstack/solid-router'
+import { render } from 'solid-js/web'
+import { getRouter } from './router'
+
+export { loaderPayloadRecordCount } from './loader-data'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const dispose = render(() => , container)
+ let didUnmount = false
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ dispose()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts
new file mode 100644
index 0000000000..3bd59c82ef
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts
@@ -0,0 +1,48 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
+
+export const loaderPayloadRecordCount = 512
+
+const loaderPayloadCharsPerRecord = 1024
+
+type LoaderRecord = {
+ key: string
+ value: string
+}
+
+export function createLoaderData(id: string) {
+ const random = createDeterministicRandom(hashId(id))
+ const records: Array = []
+
+ for (let index = 0; index < loaderPayloadRecordCount; index++) {
+ records.push({
+ key: `${id}:${index}:${randomSegment(random)}`,
+ value: createRecordValue(id, index, random),
+ })
+ }
+
+ return { id, records }
+}
+
+function createRecordValue(id: string, index: number, random: () => number) {
+ const firstSegment = randomSegment(random)
+ const secondSegment = randomSegment(random)
+ const segment = `${id}:${index}:${firstSegment}:${secondSegment}:`
+
+ return segment
+ .repeat(Math.ceil(loaderPayloadCharsPerRecord / segment.length))
+ .slice(0, loaderPayloadCharsPerRecord)
+}
+
+function hashId(id: string) {
+ let hash = 2166136261
+
+ for (let index = 0; index < id.length; index++) {
+ hash ^= id.charCodeAt(index)
+ hash = Math.imul(hash, 16777619)
+ }
+
+ return hash >>> 0
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..a1946d187d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts
@@ -0,0 +1,102 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ShellRouteImport } from './routes/shell'
+import { Route as ShellIndexRouteImport } from './routes/shell.index'
+import { Route as PageIdRouteImport } from './routes/page.$id'
+
+const ShellRoute = ShellRouteImport.update({
+ id: '/shell',
+ path: '/shell',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ShellIndexRoute = ShellIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => ShellRoute,
+} as any)
+const PageIdRoute = PageIdRouteImport.update({
+ id: '/page/$id',
+ path: '/page/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/shell': typeof ShellRouteWithChildren
+ '/page/$id': typeof PageIdRoute
+ '/shell/': typeof ShellIndexRoute
+}
+export interface FileRoutesByTo {
+ '/page/$id': typeof PageIdRoute
+ '/shell': typeof ShellIndexRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/shell': typeof ShellRouteWithChildren
+ '/page/$id': typeof PageIdRoute
+ '/shell/': typeof ShellIndexRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/shell' | '/page/$id' | '/shell/'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/page/$id' | '/shell'
+ id: '__root__' | '/shell' | '/page/$id' | '/shell/'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ShellRoute: typeof ShellRouteWithChildren
+ PageIdRoute: typeof PageIdRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/shell': {
+ id: '/shell'
+ path: '/shell'
+ fullPath: '/shell'
+ preLoaderRoute: typeof ShellRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/shell/': {
+ id: '/shell/'
+ path: '/'
+ fullPath: '/shell/'
+ preLoaderRoute: typeof ShellIndexRouteImport
+ parentRoute: typeof ShellRoute
+ }
+ '/page/$id': {
+ id: '/page/$id'
+ path: '/page/$id'
+ fullPath: '/page/$id'
+ preLoaderRoute: typeof PageIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+interface ShellRouteChildren {
+ ShellIndexRoute: typeof ShellIndexRoute
+}
+
+const ShellRouteChildren: ShellRouteChildren = {
+ ShellIndexRoute: ShellIndexRoute,
+}
+
+const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren)
+
+const rootRouteChildren: RootRouteChildren = {
+ ShellRoute: ShellRouteWithChildren,
+ PageIdRoute: PageIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx
new file mode 100644
index 0000000000..06afdb10d3
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx
@@ -0,0 +1,19 @@
+import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/shell'],
+ }),
+ routeTree,
+ defaultGcTime: 0,
+ defaultPreloadGcTime: 0,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..cb8d5a688d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx
new file mode 100644
index 0000000000..3968848038
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createLoaderData } from '../loader-data'
+
+export const Route = createFileRoute('/page/$id')({
+ loader: ({ params }) => createLoaderData(params.id),
+ component: PageComponent,
+})
+
+function PageComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data().id}:${data().records.length}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx
new file mode 100644
index 0000000000..2f4f5daee2
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/shell/')({
+ component: ShellIndexComponent,
+})
+
+function ShellIndexComponent() {
+ return ready
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx
new file mode 100644
index 0000000000..c1e38a1aaf
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/shell')({
+ component: ShellComponent,
+})
+
+function ShellComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json
new file mode 100644
index 0000000000..b12dcb7ade
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts
new file mode 100644
index 0000000000..58d6b77acc
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ solid({ hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client loader-data-retention (solid)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json
new file mode 100644
index 0000000000..22504c906f
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-loader-data-retention-vue",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts
new file mode 100644
index 0000000000..a1701d8dbf
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { loaderPayloadRecordCount, mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('vue', mountTestApp, loaderPayloadRecordCount)
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx
new file mode 100644
index 0000000000..e9d54137ae
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx
@@ -0,0 +1,38 @@
+import { RouterProvider } from '@tanstack/vue-router'
+import { createApp } from 'vue'
+import { getRouter } from './router'
+import type {} from '@tanstack/router-core'
+
+export { loaderPayloadRecordCount } from './loader-data'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const vueApp = createApp({
+ setup() {
+ return () =>
+ },
+ })
+ let didUnmount = false
+
+ vueApp.mount(container)
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ vueApp.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts
new file mode 100644
index 0000000000..3bd59c82ef
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts
@@ -0,0 +1,48 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
+
+export const loaderPayloadRecordCount = 512
+
+const loaderPayloadCharsPerRecord = 1024
+
+type LoaderRecord = {
+ key: string
+ value: string
+}
+
+export function createLoaderData(id: string) {
+ const random = createDeterministicRandom(hashId(id))
+ const records: Array = []
+
+ for (let index = 0; index < loaderPayloadRecordCount; index++) {
+ records.push({
+ key: `${id}:${index}:${randomSegment(random)}`,
+ value: createRecordValue(id, index, random),
+ })
+ }
+
+ return { id, records }
+}
+
+function createRecordValue(id: string, index: number, random: () => number) {
+ const firstSegment = randomSegment(random)
+ const secondSegment = randomSegment(random)
+ const segment = `${id}:${index}:${firstSegment}:${secondSegment}:`
+
+ return segment
+ .repeat(Math.ceil(loaderPayloadCharsPerRecord / segment.length))
+ .slice(0, loaderPayloadCharsPerRecord)
+}
+
+function hashId(id: string) {
+ let hash = 2166136261
+
+ for (let index = 0; index < id.length; index++) {
+ hash ^= id.charCodeAt(index)
+ hash = Math.imul(hash, 16777619)
+ }
+
+ return hash >>> 0
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..5b09d365e7
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts
@@ -0,0 +1,102 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ShellRouteImport } from './routes/shell'
+import { Route as PageIdRouteImport } from './routes/page.$id'
+import { Route as ShellIndexRouteImport } from './routes/shell.index'
+
+const ShellRoute = ShellRouteImport.update({
+ id: '/shell',
+ path: '/shell',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PageIdRoute = PageIdRouteImport.update({
+ id: '/page/$id',
+ path: '/page/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ShellIndexRoute = ShellIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => ShellRoute,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/shell': typeof ShellRouteWithChildren
+ '/page/$id': typeof PageIdRoute
+ '/shell/': typeof ShellIndexRoute
+}
+export interface FileRoutesByTo {
+ '/page/$id': typeof PageIdRoute
+ '/shell': typeof ShellIndexRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/shell': typeof ShellRouteWithChildren
+ '/page/$id': typeof PageIdRoute
+ '/shell/': typeof ShellIndexRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/shell' | '/page/$id' | '/shell/'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/page/$id' | '/shell'
+ id: '__root__' | '/shell' | '/page/$id' | '/shell/'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ShellRoute: typeof ShellRouteWithChildren
+ PageIdRoute: typeof PageIdRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/shell': {
+ id: '/shell'
+ path: '/shell'
+ fullPath: '/shell'
+ preLoaderRoute: typeof ShellRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/page/$id': {
+ id: '/page/$id'
+ path: '/page/$id'
+ fullPath: '/page/$id'
+ preLoaderRoute: typeof PageIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/shell/': {
+ id: '/shell/'
+ path: '/'
+ fullPath: '/shell/'
+ preLoaderRoute: typeof ShellIndexRouteImport
+ parentRoute: typeof ShellRoute
+ }
+ }
+}
+
+interface ShellRouteChildren {
+ ShellIndexRoute: typeof ShellIndexRoute
+}
+
+const ShellRouteChildren: ShellRouteChildren = {
+ ShellIndexRoute: ShellIndexRoute,
+}
+
+const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren)
+
+const rootRouteChildren: RootRouteChildren = {
+ ShellRoute: ShellRouteWithChildren,
+ PageIdRoute: PageIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx
new file mode 100644
index 0000000000..84286e1dae
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx
@@ -0,0 +1,19 @@
+import { createMemoryHistory, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/shell'],
+ }),
+ routeTree,
+ defaultGcTime: 0,
+ defaultPreloadGcTime: 0,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..91296e6f84
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx
new file mode 100644
index 0000000000..570ea3fb68
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createLoaderData } from '../loader-data'
+
+export const Route = createFileRoute('/page/$id')({
+ loader: ({ params }: { params: { id: string } }) =>
+ createLoaderData(params.id),
+ component: PageComponent,
+})
+
+function PageComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.value.id}:${data.value.records.length}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx
new file mode 100644
index 0000000000..640ffeeb5c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/shell/')({
+ component: ShellIndexComponent,
+})
+
+function ShellIndexComponent() {
+ return ready
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx
new file mode 100644
index 0000000000..f8a598bc34
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/shell')({
+ component: ShellComponent,
+})
+
+function ShellComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json
new file mode 100644
index 0000000000..9a5872a4c0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts
new file mode 100644
index 0000000000..bbf2fd02d8
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts
@@ -0,0 +1,36 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ vue(),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client loader-data-retention (vue)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
index 203dc39d37..7083cd1432 100644
--- a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts
@@ -1,73 +1,11 @@
import type * as App from './src/app'
+import { createSetup } from '../shared'
const appModulePath = './dist/app.js'
const { mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
-const mountUnmountIterations = 100
-
-function drainMicrotasks() {
- return Promise.resolve().then(() => Promise.resolve())
-}
-
-function assertEmptyBody() {
- if (document.body.childNodes.length !== 0) {
- throw new Error(
- `Expected document.body to be empty, found ${document.body.childNodes.length} child node(s)`,
- )
- }
-}
export function setup() {
- if (process.env.NODE_ENV !== 'production') {
- console.warn(
- 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
- )
- }
-
- async function cycle() {
- const container = document.createElement('div')
- document.body.append(container)
-
- let unmount = () => {}
- let unsubscribe = () => {}
-
- try {
- const mounted = mountTestApp(container)
- const { router } = mounted
- unmount = mounted.unmount
-
- const rendered = new Promise((resolve) => {
- unsubscribe = router.subscribe('onRendered', () => {
- resolve()
- })
- })
-
- await router.load()
- await rendered
- unsubscribe()
- unsubscribe = () => {}
- } finally {
- unmount()
- container.remove()
- unsubscribe()
- await drainMicrotasks()
- }
- }
-
- return {
- name: 'mem mount-unmount (react)',
- cycle,
- async run() {
- for (let index = 0; index < mountUnmountIterations; index++) {
- await cycle()
- }
- },
- async sanity() {
- assertEmptyBody()
- await cycle()
- await cycle()
- assertEmptyBody()
- },
- }
+ return createSetup('react', mountTestApp)
}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/shared.ts b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts
new file mode 100644
index 0000000000..1cc57712cd
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts
@@ -0,0 +1,90 @@
+type Framework = 'react' | 'solid' | 'vue'
+
+type MountedApp = {
+ router: unknown
+ unmount: () => void
+}
+
+type MountTestApp = (container: HTMLDivElement) => MountedApp
+
+type RenderRouter = {
+ load: () => Promise
+ subscribe: (event: 'onRendered', listener: () => void) => () => void
+}
+
+const frameworkNames = {
+ react: 'React',
+ solid: 'Solid',
+ vue: 'Vue',
+} satisfies Record
+const mountUnmountIterations = 100
+
+function warnDevMode(framework: Framework) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`,
+ )
+ }
+}
+
+function drainMicrotasks() {
+ return Promise.resolve().then(() => Promise.resolve())
+}
+
+function assertEmptyBody() {
+ if (document.body.childNodes.length !== 0) {
+ throw new Error(
+ `Expected document.body to be empty, found ${document.body.childNodes.length} child node(s)`,
+ )
+ }
+}
+
+export function createSetup(framework: Framework, mountTestApp: MountTestApp) {
+ warnDevMode(framework)
+
+ async function cycle() {
+ const container = document.createElement('div')
+ document.body.append(container)
+
+ let unmount = () => {}
+ let unsubscribe = () => {}
+
+ try {
+ const mounted = mountTestApp(container)
+ const router = mounted.router as RenderRouter
+ unmount = mounted.unmount
+
+ const rendered = new Promise((resolve) => {
+ unsubscribe = router.subscribe('onRendered', () => {
+ resolve()
+ })
+ })
+
+ await router.load()
+ await rendered
+ unsubscribe()
+ unsubscribe = () => {}
+ } finally {
+ unmount()
+ container.remove()
+ unsubscribe()
+ await drainMicrotasks()
+ }
+ }
+
+ return {
+ name: `mem mount-unmount (${framework})`,
+ cycle,
+ async run() {
+ for (let index = 0; index < mountUnmountIterations; index++) {
+ await cycle()
+ }
+ },
+ async sanity() {
+ assertEmptyBody()
+ await cycle()
+ await cycle()
+ assertEmptyBody()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json b/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json
new file mode 100644
index 0000000000..63e8e950db
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-mount-unmount-solid",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts
new file mode 100644
index 0000000000..73e5f8949e
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('solid', mountTestApp)
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx
new file mode 100644
index 0000000000..b23bc126d5
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx
@@ -0,0 +1,27 @@
+import { RouterProvider } from '@tanstack/solid-router'
+import { render } from 'solid-js/web'
+import { getRouter } from './router'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const dispose = render(() => , container)
+ let didUnmount = false
+
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ dispose()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..a3f9739290
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts
@@ -0,0 +1,59 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ARouteImport } from './routes/a'
+
+const ARoute = ARouteImport.update({
+ id: '/a',
+ path: '/a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/a': typeof ARoute
+}
+export interface FileRoutesByTo {
+ '/a': typeof ARoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/a': typeof ARoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/a'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/a'
+ id: '__root__' | '/a'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ARoute: typeof ARoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/a': {
+ id: '/a'
+ path: '/a'
+ fullPath: '/a'
+ preLoaderRoute: typeof ARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ARoute: ARoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx
new file mode 100644
index 0000000000..e8c14f12f6
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/a'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..cb8d5a688d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx
new file mode 100644
index 0000000000..36c4be538c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/a')({
+ loader: () => ({ id: 'a' }),
+ component: AComponent,
+})
+
+function AComponent() {
+ const data = Route.useLoaderData()
+
+ return {data().id}
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json
new file mode 100644
index 0000000000..b12dcb7ade
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts
new file mode 100644
index 0000000000..5f0ab26252
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ solid({ hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client mount-unmount (solid)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json b/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json
new file mode 100644
index 0000000000..157673cf16
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-mount-unmount-vue",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts
new file mode 100644
index 0000000000..0f76d289d4
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('vue', mountTestApp)
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx
new file mode 100644
index 0000000000..27b82fcbe1
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx
@@ -0,0 +1,34 @@
+import { RouterProvider } from '@tanstack/vue-router'
+import { createApp } from 'vue'
+import { getRouter } from './router'
+import type {} from '@tanstack/router-core'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const vueApp = createApp({
+ setup() {
+ return () =>
+ },
+ })
+ let didUnmount = false
+
+ vueApp.mount(container)
+
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ vueApp.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..1a54b81033
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts
@@ -0,0 +1,59 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ARouteImport } from './routes/a'
+
+const ARoute = ARouteImport.update({
+ id: '/a',
+ path: '/a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/a': typeof ARoute
+}
+export interface FileRoutesByTo {
+ '/a': typeof ARoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/a': typeof ARoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/a'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/a'
+ id: '__root__' | '/a'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ARoute: typeof ARoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/a': {
+ id: '/a'
+ path: '/a'
+ fullPath: '/a'
+ preLoaderRoute: typeof ARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ARoute: ARoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx
new file mode 100644
index 0000000000..e083495cd2
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/a'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..91296e6f84
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx
new file mode 100644
index 0000000000..cbcd543ce5
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/a')({
+ loader: () => ({ id: 'a' }),
+ component: AComponent,
+})
+
+function AComponent() {
+ const data = Route.useLoaderData()
+
+ return {data.value.id}
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json
new file mode 100644
index 0000000000..9a5872a4c0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts
new file mode 100644
index 0000000000..0f7573a408
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts
@@ -0,0 +1,36 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ vue(),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client mount-unmount (vue)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
index deac1d1bbf..7083cd1432 100644
--- a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts
@@ -1,124 +1,11 @@
import type * as App from './src/app'
-
-type Target = '/a' | '/b'
+import { createSetup } from '../shared'
const appModulePath = './dist/app.js'
const { mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
-const navigationChurnIterations = 300
-
-const uninitialized = () =>
- Promise.reject(new Error('navigation-churn benchmark is not initialized'))
export function setup() {
- if (process.env.NODE_ENV !== 'production') {
- console.warn(
- 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
- )
- }
-
- let container: HTMLDivElement | undefined = undefined
- let unmount: (() => void) | undefined = undefined
- let unsub = () => {}
- let resolveRendered: () => void = () => {}
- let navigateTo: (target: Target) => Promise = uninitialized
-
- function assertRenderedPage(target: Target) {
- const expected = target.slice(1)
- const actual =
- container?.querySelector('[data-bench-page]')?.dataset
- .benchPage
-
- if (actual !== expected) {
- throw new Error(`Expected rendered page ${expected}, got ${actual}`)
- }
- }
-
- async function waitForRenderedPage(target: Target) {
- for (let attempt = 0; attempt < 10; attempt++) {
- try {
- assertRenderedPage(target)
- return
- } catch {
- await new Promise((resolve) => {
- requestAnimationFrame(() => resolve())
- })
- }
- }
-
- assertRenderedPage(target)
- }
-
- function waitForNextRender() {
- return new Promise((resolve) => {
- resolveRendered = resolve
- })
- }
-
- async function before() {
- if (container) {
- after()
- }
-
- container = document.createElement('div')
- document.body.append(container)
-
- const mounted = mountTestApp(container)
- const { router } = mounted
- unmount = mounted.unmount
-
- unsub = router.subscribe('onRendered', () => {
- resolveRendered()
- })
-
- navigateTo = async (target) => {
- const rendered = waitForNextRender()
- await Promise.all([
- router.navigate({
- to: target,
- replace: true,
- }),
- rendered,
- ])
- }
-
- await router.load()
- await waitForRenderedPage('/a')
- }
-
- function after() {
- unmount?.()
- container?.remove()
- unsub()
-
- container = undefined
- unmount = undefined
- unsub = () => {}
- resolveRendered = () => {}
- navigateTo = uninitialized
- }
-
- return {
- name: 'mem navigation-churn (react)',
- before,
- navigate: (target: Target) => navigateTo(target),
- async run() {
- for (let index = 0; index < navigationChurnIterations; index++) {
- await navigateTo(index % 2 === 0 ? '/b' : '/a')
- }
- },
- async sanity() {
- await before()
-
- try {
- assertRenderedPage('/a')
- await navigateTo('/b')
- assertRenderedPage('/b')
- } finally {
- after()
- }
- },
- after,
- }
+ return createSetup('react', mountTestApp)
}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/shared.ts b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts
new file mode 100644
index 0000000000..ab36c48767
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts
@@ -0,0 +1,141 @@
+type Framework = 'react' | 'solid' | 'vue'
+type Target = '/a' | '/b'
+
+type MountedApp = {
+ router: unknown
+ unmount: () => void
+}
+
+type MountTestApp = (container: HTMLDivElement) => MountedApp
+
+type NavigationRouter = {
+ load: () => Promise
+ navigate: (options: { to: Target; replace: true }) => Promise
+ subscribe: (event: 'onRendered', listener: () => void) => () => void
+}
+
+const frameworkNames = {
+ react: 'React',
+ solid: 'Solid',
+ vue: 'Vue',
+} satisfies Record
+const navigationChurnIterations = 300
+
+const uninitialized = () =>
+ Promise.reject(new Error('navigation-churn benchmark is not initialized'))
+
+function warnDevMode(framework: Framework) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`,
+ )
+ }
+}
+
+export function createSetup(framework: Framework, mountTestApp: MountTestApp) {
+ warnDevMode(framework)
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let navigateTo: (target: Target) => Promise = uninitialized
+
+ function assertRenderedPage(target: Target) {
+ const expected = target.slice(1)
+ const actual =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+
+ if (actual !== expected) {
+ throw new Error(`Expected rendered page ${expected}, got ${actual}`)
+ }
+ }
+
+ async function waitForRenderedPage(target: Target) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedPage(target)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedPage(target)
+ }
+
+ function waitForNextRender() {
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const router = mounted.router as NavigationRouter
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', () => {
+ resolveRendered()
+ })
+
+ navigateTo = async (target) => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router.navigate({
+ to: target,
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ await router.load()
+ await waitForRenderedPage('/a')
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ navigateTo = uninitialized
+ }
+
+ return {
+ name: `mem navigation-churn (${framework})`,
+ before,
+ navigate: (target: Target) => navigateTo(target),
+ async run() {
+ for (let index = 0; index < navigationChurnIterations; index++) {
+ await navigateTo(index % 2 === 0 ? '/b' : '/a')
+ }
+ },
+ async sanity() {
+ await before()
+
+ try {
+ assertRenderedPage('/a')
+ await navigateTo('/b')
+ assertRenderedPage('/b')
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json b/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json
new file mode 100644
index 0000000000..2c581e6188
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-navigation-churn-solid",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts
new file mode 100644
index 0000000000..73e5f8949e
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('solid', mountTestApp)
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx
new file mode 100644
index 0000000000..16000c90a1
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx
@@ -0,0 +1,29 @@
+import { RouterProvider } from '@tanstack/solid-router'
+import { render } from 'solid-js/web'
+import { getRouter } from './router'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const dispose = render(() => , container)
+ let didUnmount = false
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ dispose()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..59d7fad058
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts
@@ -0,0 +1,77 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as BRouteImport } from './routes/b'
+import { Route as ARouteImport } from './routes/a'
+
+const BRoute = BRouteImport.update({
+ id: '/b',
+ path: '/b',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ARoute = ARouteImport.update({
+ id: '/a',
+ path: '/a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRoutesByTo {
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/a' | '/b'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/a' | '/b'
+ id: '__root__' | '/a' | '/b'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ARoute: typeof ARoute
+ BRoute: typeof BRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/b': {
+ id: '/b'
+ path: '/b'
+ fullPath: '/b'
+ preLoaderRoute: typeof BRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/a': {
+ id: '/a'
+ path: '/a'
+ fullPath: '/a'
+ preLoaderRoute: typeof ARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ARoute: ARoute,
+ BRoute: BRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx
new file mode 100644
index 0000000000..e8c14f12f6
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/a'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..cb8d5a688d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx
new file mode 100644
index 0000000000..0b7838fdbe
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+const fixedTimestamp = 1_700_000_000_000
+
+export const Route = createFileRoute('/a')({
+ loader: () => ({ name: 'a', ts: fixedTimestamp }),
+ component: AComponent,
+})
+
+function AComponent() {
+ const data = Route.useLoaderData()
+
+ return {`${data().name}:${data().ts}`}
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx
new file mode 100644
index 0000000000..ec9b513ba9
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+const fixedTimestamp = 1_700_000_000_000
+
+export const Route = createFileRoute('/b')({
+ loader: () => ({ name: 'b', ts: fixedTimestamp }),
+ component: BComponent,
+})
+
+function BComponent() {
+ const data = Route.useLoaderData()
+
+ return {`${data().name}:${data().ts}`}
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json
new file mode 100644
index 0000000000..b12dcb7ade
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts
new file mode 100644
index 0000000000..6b6735891a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ solid({ hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client navigation-churn (solid)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json b/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json
new file mode 100644
index 0000000000..f96d03d7d3
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-navigation-churn-vue",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts
new file mode 100644
index 0000000000..0f76d289d4
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('vue', mountTestApp)
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx
new file mode 100644
index 0000000000..fad2251cc0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx
@@ -0,0 +1,36 @@
+import { RouterProvider } from '@tanstack/vue-router'
+import { createApp } from 'vue'
+import { getRouter } from './router'
+import type {} from '@tanstack/router-core'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const vueApp = createApp({
+ setup() {
+ return () =>
+ },
+ })
+ let didUnmount = false
+
+ vueApp.mount(container)
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ vueApp.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..12c9640358
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts
@@ -0,0 +1,77 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as BRouteImport } from './routes/b'
+import { Route as ARouteImport } from './routes/a'
+
+const BRoute = BRouteImport.update({
+ id: '/b',
+ path: '/b',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ARoute = ARouteImport.update({
+ id: '/a',
+ path: '/a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRoutesByTo {
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/a': typeof ARoute
+ '/b': typeof BRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/a' | '/b'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/a' | '/b'
+ id: '__root__' | '/a' | '/b'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ARoute: typeof ARoute
+ BRoute: typeof BRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/b': {
+ id: '/b'
+ path: '/b'
+ fullPath: '/b'
+ preLoaderRoute: typeof BRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/a': {
+ id: '/a'
+ path: '/a'
+ fullPath: '/a'
+ preLoaderRoute: typeof ARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ARoute: ARoute,
+ BRoute: BRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx
new file mode 100644
index 0000000000..e083495cd2
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/a'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..91296e6f84
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx
new file mode 100644
index 0000000000..a1537dc324
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+const fixedTimestamp = 1_700_000_000_000
+
+export const Route = createFileRoute('/a')({
+ loader: () => ({ name: 'a', ts: fixedTimestamp }),
+ component: AComponent,
+})
+
+function AComponent() {
+ const data = Route.useLoaderData()
+
+ return {`${data.value.name}:${data.value.ts}`}
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx
new file mode 100644
index 0000000000..ce19d1bd10
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+const fixedTimestamp = 1_700_000_000_000
+
+export const Route = createFileRoute('/b')({
+ loader: () => ({ name: 'b', ts: fixedTimestamp }),
+ component: BComponent,
+})
+
+function BComponent() {
+ const data = Route.useLoaderData()
+
+ return {`${data.value.name}:${data.value.ts}`}
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json
new file mode 100644
index 0000000000..9a5872a4c0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts
new file mode 100644
index 0000000000..d26ce23d07
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts
@@ -0,0 +1,36 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ vue(),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client navigation-churn (vue)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
index a1737c644e..d7c39e5e01 100644
--- a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts
@@ -1,266 +1,11 @@
import type * as App from './src/app'
-import {
- createDeterministicRandom,
- randomSegment,
-} from '#memory-client/bench-utils'
+import { createSetup } from '../shared'
const appModulePath = './dist/app.js'
const { getTrackedItemLoaderCount, mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
-type MountedApp = ReturnType
-
-// Fixed id for the eviction navigations interleaved into the bench loop; its
-// payload is a constant-size steady-state resident, never part of the signal.
-const evictionItemId = 'nav-evict'
-const preloadChurnIterations = 200
-// A navigation commit is what triggers the router's clearExpiredCache --
-// preloaded matches (defaultPreloadGcTime: 0) are only evicted then, never
-// during a preload-only loop. Interleaving a navigation every few preloads is
-// what makes the flat floor assert "eviction releases preloaded payloads".
-const preloadsPerEvictionNavigation = 10
-// Module-level so ids stay unique across runner invocations on one mount; a
-// per-invocation LCG would replay identical ids, and every preload after the
-// first invocation would dedupe against cachedMatches instead of doing work.
-const benchmarkRandom = createDeterministicRandom(0x706c6f61)
-let preloadCounter = 0
-
-const uninitialized = async (_id: string) => {
- throw new Error('preload-churn benchmark is not initialized')
-}
-
-async function drainMicrotasks() {
- await Promise.resolve()
- await Promise.resolve()
-}
-
export function setup() {
- if (process.env.NODE_ENV !== 'production') {
- console.warn(
- 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
- )
- }
-
- let container: HTMLDivElement | undefined = undefined
- let router: MountedApp['router'] | undefined = undefined
- let unmount: (() => void) | undefined = undefined
- let unsub = () => {}
- let resolveRendered: () => void = () => {}
- let evictionParity = 0
- let preloadItem: (id: string) => Promise = uninitialized
- let navigateToItem: (id: string) => Promise = uninitialized
- let navigateToIndex: () => Promise = () =>
- uninitialized('navigate-to-index')
-
- function assertRenderedIndex() {
- const actual =
- container?.querySelector('[data-bench-page]')?.dataset
- .benchPage
-
- if (actual !== 'index') {
- throw new Error(`Expected rendered index page, got ${actual}`)
- }
- }
-
- function assertRenderedItem(id: string) {
- const page =
- container?.querySelector('[data-bench-page]')?.dataset
- .benchPage
- const actualId =
- container?.querySelector('[data-bench-id]')?.dataset.benchId
-
- if (page !== 'item' || actualId !== id) {
- throw new Error(`Expected rendered item ${id}, got ${page}:${actualId}`)
- }
- }
-
- function hasCachedItemMatch(id: string) {
- return Boolean(
- router?.stores.cachedMatches
- .get()
- .some((match) => (match.params as { id?: string }).id === id),
- )
- }
-
- async function waitForRenderedIndex() {
- for (let attempt = 0; attempt < 10; attempt++) {
- try {
- assertRenderedIndex()
- return
- } catch {
- await new Promise((resolve) => {
- requestAnimationFrame(() => resolve())
- })
- }
- }
-
- assertRenderedIndex()
- }
-
- async function waitForRenderedItem(id: string) {
- for (let attempt = 0; attempt < 10; attempt++) {
- try {
- assertRenderedItem(id)
- return
- } catch {
- await new Promise((resolve) => {
- requestAnimationFrame(() => resolve())
- })
- }
- }
-
- assertRenderedItem(id)
- }
-
- function waitForNextRender() {
- return new Promise((resolve) => {
- resolveRendered = resolve
- })
- }
-
- async function before() {
- if (container) {
- after()
- }
-
- container = document.createElement('div')
- document.body.append(container)
-
- const mounted = mountTestApp(container)
- router = mounted.router
- unmount = mounted.unmount
-
- unsub = router.subscribe('onRendered', () => {
- resolveRendered()
- })
-
- preloadItem = async (id) => {
- await router!.preloadRoute({
- to: '/items/$id',
- params: { id },
- })
- await drainMicrotasks()
- }
-
- navigateToItem = async (id) => {
- const rendered = waitForNextRender()
- await Promise.all([
- router!.navigate({
- to: '/items/$id',
- params: { id },
- replace: true,
- }),
- rendered,
- ])
- }
-
- navigateToIndex = async () => {
- const rendered = waitForNextRender()
- await Promise.all([
- router!.navigate({
- to: '/',
- replace: true,
- }),
- rendered,
- ])
- }
-
- await router.load()
- await waitForRenderedIndex()
- }
-
- function after() {
- unmount?.()
- container?.remove()
- unsub()
-
- container = undefined
- router = undefined
- unmount = undefined
- unsub = () => {}
- resolveRendered = () => {}
- evictionParity = 0
- preloadItem = uninitialized
- navigateToItem = uninitialized
- navigateToIndex = () => uninitialized('navigate-to-index')
- }
-
- // Alternate between two distinct locations so every eviction navigation
- // changes the href (a same-href navigate would never commit or render).
- async function evictPreloads() {
- evictionParity = (evictionParity + 1) % 2
-
- if (evictionParity === 1) {
- await navigateToItem(evictionItemId)
- } else {
- await navigateToIndex()
- }
- }
-
- return {
- name: 'mem preload-churn (react)',
- before,
- preload: (id: string) => preloadItem(id),
- evictPreloads,
- async run() {
- for (let index = 0; index < preloadChurnIterations; index++) {
- await preloadItem(
- `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`,
- )
-
- if ((index + 1) % preloadsPerEvictionNavigation === 0) {
- await evictPreloads()
- }
- }
- },
- async sanity() {
- await before()
-
- try {
- assertRenderedIndex()
-
- const id = 'sanity-preloaded-item'
- const initialLoaderCount = getTrackedItemLoaderCount(id)
- await preloadItem(id)
-
- const preloadedLoaderCount = getTrackedItemLoaderCount(id)
- if (preloadedLoaderCount !== initialLoaderCount + 1) {
- throw new Error(
- `Expected preload to run item loader once, got ${preloadedLoaderCount - initialLoaderCount}`,
- )
- }
-
- if (!hasCachedItemMatch(id)) {
- throw new Error(
- 'Expected preloaded match to sit in router.state.cachedMatches',
- )
- }
-
- // A navigation commit runs clearExpiredCache; with
- // defaultPreloadGcTime: 0 it must evict the preloaded match. This is
- // the mechanism the bench's flat-floor expectation rests on.
- await navigateToItem('sanity-evict-nav')
- await waitForRenderedItem('sanity-evict-nav')
-
- if (hasCachedItemMatch(id)) {
- throw new Error(
- 'Expected the navigation commit to evict the preloaded match (preloadGcTime 0)',
- )
- }
-
- await preloadItem(id)
-
- const repreloadedLoaderCount = getTrackedItemLoaderCount(id)
- if (repreloadedLoaderCount !== preloadedLoaderCount + 1) {
- throw new Error(
- 'Expected re-preload after eviction to run the item loader again',
- )
- }
- } finally {
- after()
- }
- },
- after,
- }
+ return createSetup('react', mountTestApp, getTrackedItemLoaderCount)
}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/shared.ts b/benchmarks/memory/client/scenarios/preload-churn/shared.ts
new file mode 100644
index 0000000000..09784fc311
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/shared.ts
@@ -0,0 +1,308 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
+
+type Framework = 'react' | 'solid' | 'vue'
+
+type MountedApp = {
+ router: unknown
+ unmount: () => void
+}
+
+type MountTestApp = (container: HTMLDivElement) => MountedApp
+type GetTrackedItemLoaderCount = (id: string) => number
+
+type PreloadRouter = {
+ load: () => Promise
+ preloadRoute: (options: {
+ to: '/items/$id'
+ params: { id: string }
+ }) => Promise
+ navigate: (
+ options:
+ | {
+ to: '/items/$id'
+ params: { id: string }
+ replace: true
+ }
+ | {
+ to: '/'
+ replace: true
+ },
+ ) => Promise
+ subscribe: (event: 'onRendered', listener: () => void) => () => void
+ stores: {
+ cachedMatches: {
+ get: () => unknown
+ }
+ }
+}
+
+const frameworkNames = {
+ react: 'React',
+ solid: 'Solid',
+ vue: 'Vue',
+} satisfies Record
+
+// Fixed id for the eviction navigations interleaved into the bench loop; its
+// payload is a constant-size steady-state resident, never part of the signal.
+const evictionItemId = 'nav-evict'
+const preloadChurnIterations = 200
+// A navigation commit is what triggers the router's clearExpiredCache --
+// preloaded matches (defaultPreloadGcTime: 0) are only evicted then, never
+// during a preload-only loop. Interleaving a navigation every few preloads is
+// what makes the flat floor assert "eviction releases preloaded payloads".
+const preloadsPerEvictionNavigation = 10
+// Module-level so ids stay unique across runner invocations on one mount; a
+// per-invocation LCG would replay identical ids, and every preload after the
+// first invocation would dedupe against cachedMatches instead of doing work.
+const benchmarkRandom = createDeterministicRandom(0x706c6f61)
+let preloadCounter = 0
+
+const uninitialized = async (_id: string) => {
+ throw new Error('preload-churn benchmark is not initialized')
+}
+
+function warnDevMode(framework: Framework) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`,
+ )
+ }
+}
+
+async function drainMicrotasks() {
+ await Promise.resolve()
+ await Promise.resolve()
+}
+
+export function createSetup(
+ framework: Framework,
+ mountTestApp: MountTestApp,
+ getTrackedItemLoaderCount: GetTrackedItemLoaderCount,
+) {
+ warnDevMode(framework)
+
+ let container: HTMLDivElement | undefined = undefined
+ let router: PreloadRouter | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let evictionParity = 0
+ let preloadItem: (id: string) => Promise = uninitialized
+ let navigateToItem: (id: string) => Promise = uninitialized
+ let navigateToIndex: () => Promise = () =>
+ uninitialized('navigate-to-index')
+
+ function assertRenderedIndex() {
+ const actual =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+
+ if (actual !== 'index') {
+ throw new Error(`Expected rendered index page, got ${actual}`)
+ }
+ }
+
+ function assertRenderedItem(id: string) {
+ const page =
+ container?.querySelector('[data-bench-page]')?.dataset
+ .benchPage
+ const actualId =
+ container?.querySelector('[data-bench-id]')?.dataset.benchId
+
+ if (page !== 'item' || actualId !== id) {
+ throw new Error(`Expected rendered item ${id}, got ${page}:${actualId}`)
+ }
+ }
+
+ function hasCachedItemMatch(id: string) {
+ const cachedMatches = router?.stores.cachedMatches.get() as
+ | Array<{ params: { id?: string } }>
+ | undefined
+
+ return Boolean(cachedMatches?.some((match) => match.params.id === id))
+ }
+
+ async function waitForRenderedIndex() {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedIndex()
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedIndex()
+ }
+
+ async function waitForRenderedItem(id: string) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedItem(id)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedItem(id)
+ }
+
+ function waitForNextRender() {
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ router = mounted.router as PreloadRouter
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', () => {
+ resolveRendered()
+ })
+
+ preloadItem = async (id) => {
+ await router!.preloadRoute({
+ to: '/items/$id',
+ params: { id },
+ })
+ await drainMicrotasks()
+ }
+
+ navigateToItem = async (id) => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router!.navigate({
+ to: '/items/$id',
+ params: { id },
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ navigateToIndex = async () => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router!.navigate({
+ to: '/',
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ await router.load()
+ await waitForRenderedIndex()
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ router = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ evictionParity = 0
+ preloadItem = uninitialized
+ navigateToItem = uninitialized
+ navigateToIndex = () => uninitialized('navigate-to-index')
+ }
+
+ // Alternate between two distinct locations so every eviction navigation
+ // changes the href (a same-href navigate would never commit or render).
+ async function evictPreloads() {
+ evictionParity = (evictionParity + 1) % 2
+
+ if (evictionParity === 1) {
+ await navigateToItem(evictionItemId)
+ } else {
+ await navigateToIndex()
+ }
+ }
+
+ return {
+ name: `mem preload-churn (${framework})`,
+ before,
+ preload: (id: string) => preloadItem(id),
+ evictPreloads,
+ async run() {
+ for (let index = 0; index < preloadChurnIterations; index++) {
+ await preloadItem(
+ `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`,
+ )
+
+ if ((index + 1) % preloadsPerEvictionNavigation === 0) {
+ await evictPreloads()
+ }
+ }
+ },
+ async sanity() {
+ await before()
+
+ try {
+ assertRenderedIndex()
+
+ const id = 'sanity-preloaded-item'
+ const initialLoaderCount = getTrackedItemLoaderCount(id)
+ await preloadItem(id)
+
+ const preloadedLoaderCount = getTrackedItemLoaderCount(id)
+ if (preloadedLoaderCount !== initialLoaderCount + 1) {
+ throw new Error(
+ `Expected preload to run item loader once, got ${preloadedLoaderCount - initialLoaderCount}`,
+ )
+ }
+
+ if (!hasCachedItemMatch(id)) {
+ throw new Error(
+ 'Expected preloaded match to sit in router.state.cachedMatches',
+ )
+ }
+
+ // A navigation commit runs clearExpiredCache; with
+ // defaultPreloadGcTime: 0 it must evict the preloaded match. This is
+ // the mechanism the bench's flat-floor expectation rests on.
+ await navigateToItem('sanity-evict-nav')
+ await waitForRenderedItem('sanity-evict-nav')
+
+ if (hasCachedItemMatch(id)) {
+ throw new Error(
+ 'Expected the navigation commit to evict the preloaded match (preloadGcTime 0)',
+ )
+ }
+
+ await preloadItem(id)
+
+ const repreloadedLoaderCount = getTrackedItemLoaderCount(id)
+ if (repreloadedLoaderCount !== preloadedLoaderCount + 1) {
+ throw new Error(
+ 'Expected re-preload after eviction to run the item loader again',
+ )
+ }
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/project.json b/benchmarks/memory/client/scenarios/preload-churn/solid/project.json
new file mode 100644
index 0000000000..16ef8af968
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-preload-churn-solid",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts
new file mode 100644
index 0000000000..0fdbe25998
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { getTrackedItemLoaderCount, mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('solid', mountTestApp, getTrackedItemLoaderCount)
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx
new file mode 100644
index 0000000000..3dfe005e5c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx
@@ -0,0 +1,31 @@
+import { RouterProvider } from '@tanstack/solid-router'
+import { render } from 'solid-js/web'
+import { getRouter } from './router'
+
+export { getTrackedItemLoaderCount } from './item-payload'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const dispose = render(() => , container)
+ let didUnmount = false
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ dispose()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts
new file mode 100644
index 0000000000..4a86ea7244
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts
@@ -0,0 +1,51 @@
+const payloadByteLength = 20 * 1024
+const payloadChunkCount = 32
+const payloadChunkSize = payloadByteLength / payloadChunkCount
+const trackedLoaderIdPrefix = 'sanity-'
+const trackedItemLoaderCalls = new Map()
+
+export function createItemPayload(id: string) {
+ let seed = hashId(id)
+ const chunks = new Array(payloadChunkCount)
+
+ for (let index = 0; index < payloadChunkCount; index++) {
+ seed = (seed * 1664525 + 1013904223) >>> 0
+ chunks[index] = createPayloadChunk(id, index, seed)
+ }
+
+ return {
+ id,
+ chunks,
+ byteLength: payloadByteLength,
+ }
+}
+
+export function trackItemLoaderCall(id: string) {
+ if (!id.startsWith(trackedLoaderIdPrefix)) {
+ return
+ }
+
+ trackedItemLoaderCalls.set(id, (trackedItemLoaderCalls.get(id) ?? 0) + 1)
+}
+
+export function getTrackedItemLoaderCount(id: string) {
+ return trackedItemLoaderCalls.get(id) ?? 0
+}
+
+function createPayloadChunk(id: string, index: number, seed: number) {
+ const token = `${id}:${index.toString(36)}:${seed.toString(36)}:`
+ const repeatCount = Math.ceil(payloadChunkSize / token.length)
+
+ return token.repeat(repeatCount).slice(0, payloadChunkSize)
+}
+
+function hashId(id: string) {
+ let hash = 2166136261
+
+ for (let index = 0; index < id.length; index++) {
+ hash ^= id.charCodeAt(index)
+ hash = Math.imul(hash, 16777619)
+ }
+
+ return hash >>> 0
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..27749ab2af
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts
@@ -0,0 +1,77 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/items/$id'
+ id: '__root__' | '/' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx
new file mode 100644
index 0000000000..faed87f1f4
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx
@@ -0,0 +1,18 @@
+import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree,
+ defaultPreloadGcTime: 0,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..cb8d5a688d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx
new file mode 100644
index 0000000000..b870b8bfd5
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return index
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx
new file mode 100644
index 0000000000..158a6213ac
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createItemPayload, trackItemLoaderCall } from '../item-payload'
+
+export const Route = createFileRoute('/items/$id')({
+ loader: ({ params }) => {
+ trackItemLoaderCall(params.id)
+ return createItemPayload(params.id)
+ },
+ component: ItemComponent,
+})
+
+function ItemComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data().id}:${data().byteLength}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json
new file mode 100644
index 0000000000..b12dcb7ade
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts
new file mode 100644
index 0000000000..c86a6fcbb3
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ solid({ hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client preload-churn (solid)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/project.json b/benchmarks/memory/client/scenarios/preload-churn/vue/project.json
new file mode 100644
index 0000000000..e13388890f
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-preload-churn-vue",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts
new file mode 100644
index 0000000000..ab21eb1030
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { getTrackedItemLoaderCount, mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('vue', mountTestApp, getTrackedItemLoaderCount)
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx
new file mode 100644
index 0000000000..8040fc57f9
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx
@@ -0,0 +1,38 @@
+import { RouterProvider } from '@tanstack/vue-router'
+import { createApp } from 'vue'
+import { getRouter } from './router'
+import type {} from '@tanstack/router-core'
+
+export { getTrackedItemLoaderCount } from './item-payload'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const vueApp = createApp({
+ setup() {
+ return () =>
+ },
+ })
+ let didUnmount = false
+
+ vueApp.mount(container)
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ vueApp.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts
new file mode 100644
index 0000000000..4a86ea7244
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts
@@ -0,0 +1,51 @@
+const payloadByteLength = 20 * 1024
+const payloadChunkCount = 32
+const payloadChunkSize = payloadByteLength / payloadChunkCount
+const trackedLoaderIdPrefix = 'sanity-'
+const trackedItemLoaderCalls = new Map()
+
+export function createItemPayload(id: string) {
+ let seed = hashId(id)
+ const chunks = new Array(payloadChunkCount)
+
+ for (let index = 0; index < payloadChunkCount; index++) {
+ seed = (seed * 1664525 + 1013904223) >>> 0
+ chunks[index] = createPayloadChunk(id, index, seed)
+ }
+
+ return {
+ id,
+ chunks,
+ byteLength: payloadByteLength,
+ }
+}
+
+export function trackItemLoaderCall(id: string) {
+ if (!id.startsWith(trackedLoaderIdPrefix)) {
+ return
+ }
+
+ trackedItemLoaderCalls.set(id, (trackedItemLoaderCalls.get(id) ?? 0) + 1)
+}
+
+export function getTrackedItemLoaderCount(id: string) {
+ return trackedItemLoaderCalls.get(id) ?? 0
+}
+
+function createPayloadChunk(id: string, index: number, seed: number) {
+ const token = `${id}:${index.toString(36)}:${seed.toString(36)}:`
+ const repeatCount = Math.ceil(payloadChunkSize / token.length)
+
+ return token.repeat(repeatCount).slice(0, payloadChunkSize)
+}
+
+function hashId(id: string) {
+ let hash = 2166136261
+
+ for (let index = 0; index < id.length; index++) {
+ hash ^= id.charCodeAt(index)
+ hash = Math.imul(hash, 16777619)
+ }
+
+ return hash >>> 0
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..a2216f2b67
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts
@@ -0,0 +1,77 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/items/$id'
+ id: '__root__' | '/' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx
new file mode 100644
index 0000000000..86534112d7
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx
@@ -0,0 +1,18 @@
+import { createMemoryHistory, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree,
+ defaultPreloadGcTime: 0,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..91296e6f84
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx
new file mode 100644
index 0000000000..b4f871306c
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return index
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx
new file mode 100644
index 0000000000..4a39ea7bfd
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createItemPayload, trackItemLoaderCall } from '../item-payload'
+
+export const Route = createFileRoute('/items/$id')({
+ loader: ({ params }: { params: { id: string } }) => {
+ trackItemLoaderCall(params.id)
+ return createItemPayload(params.id)
+ },
+ component: ItemComponent,
+})
+
+function ItemComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {`${data.value.id}:${data.value.byteLength}`}
+
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json
new file mode 100644
index 0000000000..9a5872a4c0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts
new file mode 100644
index 0000000000..ea7ed7b695
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts
@@ -0,0 +1,36 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ vue(),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client preload-churn (vue)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
index 14fbfe7081..7083cd1432 100644
--- a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts
@@ -1,141 +1,11 @@
import type * as App from './src/app'
-import {
- createDeterministicRandom,
- randomSegment,
-} from '#memory-client/bench-utils'
-
-type ItemLocation = {
- id: string
- q: string
-}
+import { createSetup } from '../shared'
const appModulePath = './dist/app.js'
const { mountTestApp } = (await import(
/* @vite-ignore */ appModulePath
)) as typeof App
-const benchmarkRandom = createDeterministicRandom(0xdecafbad)
-const uniqueLocationChurnIterations = 300
-// Module-level so ids stay unique across runner invocations on one mount; the
-// counter prefix removes any residual LCG birthday-collision risk.
-let locationCounter = 0
-
-const uninitialized = () =>
- Promise.reject(
- new Error('unique-location-churn benchmark is not initialized'),
- )
export function setup() {
- if (process.env.NODE_ENV !== 'production') {
- console.warn(
- 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
- )
- }
-
- let container: HTMLDivElement | undefined = undefined
- let unmount: (() => void) | undefined = undefined
- let unsub = () => {}
- let resolveRendered: () => void = () => {}
- let navigateTo: (location: ItemLocation) => Promise = uninitialized
-
- function assertRenderedId(expected: string) {
- const actual =
- container?.querySelector('[data-bench-id]')?.dataset.benchId
-
- if (actual !== expected) {
- throw new Error(`Expected rendered item id ${expected}, got ${actual}`)
- }
- }
-
- async function waitForRenderedId(expected: string) {
- for (let attempt = 0; attempt < 10; attempt++) {
- try {
- assertRenderedId(expected)
- return
- } catch {
- await new Promise((resolve) => {
- requestAnimationFrame(() => resolve())
- })
- }
- }
-
- assertRenderedId(expected)
- }
-
- function waitForNextRender() {
- return new Promise((resolve) => {
- resolveRendered = resolve
- })
- }
-
- async function before() {
- if (container) {
- after()
- }
-
- container = document.createElement('div')
- document.body.append(container)
-
- const mounted = mountTestApp(container)
- const { router } = mounted
- unmount = mounted.unmount
-
- unsub = router.subscribe('onRendered', () => {
- resolveRendered()
- })
-
- navigateTo = async (location) => {
- const rendered = waitForNextRender()
- await Promise.all([
- router.navigate({
- to: '/items/$id',
- params: { id: location.id },
- search: { q: location.q },
- replace: true,
- }),
- rendered,
- ])
- }
-
- await router.load()
- await waitForRenderedId('initial')
- }
-
- function after() {
- unmount?.()
- container?.remove()
- unsub()
-
- container = undefined
- unmount = undefined
- unsub = () => {}
- resolveRendered = () => {}
- navigateTo = uninitialized
- }
-
- return {
- name: 'mem unique-location-churn (react)',
- before,
- navigate: (location: ItemLocation) => navigateTo(location),
- async run() {
- for (let index = 0; index < uniqueLocationChurnIterations; index++) {
- const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`
- const q = `q-${randomSegment(benchmarkRandom)}`
-
- await navigateTo({ id, q })
- }
- },
- async sanity() {
- await before()
-
- try {
- await navigateTo({ id: 'sanity-one', q: 'q-sanity-one' })
- assertRenderedId('sanity-one')
- await navigateTo({ id: 'sanity-two', q: 'q-sanity-two' })
- assertRenderedId('sanity-two')
- } finally {
- after()
- }
- },
- after,
- }
+ return createSetup('react', mountTestApp)
}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts
new file mode 100644
index 0000000000..272882540e
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts
@@ -0,0 +1,165 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+} from '#memory-client/bench-utils'
+
+type Framework = 'react' | 'solid' | 'vue'
+
+type ItemLocation = {
+ id: string
+ q: string
+}
+
+type MountedApp = {
+ router: unknown
+ unmount: () => void
+}
+
+type MountTestApp = (container: HTMLDivElement) => MountedApp
+
+type NavigationRouter = {
+ load: () => Promise
+ navigate: (options: {
+ to: '/items/$id'
+ params: { id: string }
+ search: { q: string }
+ replace: true
+ }) => Promise
+ subscribe: (event: 'onRendered', listener: () => void) => () => void
+}
+
+const frameworkNames = {
+ react: 'React',
+ solid: 'Solid',
+ vue: 'Vue',
+} satisfies Record
+const uniqueLocationChurnIterations = 300
+// Module-level so ids stay unique across runner invocations on one mount; the
+// counter prefix removes any residual LCG birthday-collision risk.
+const benchmarkRandom = createDeterministicRandom(0xdecafbad)
+let locationCounter = 0
+
+const uninitialized = () =>
+ Promise.reject(
+ new Error('unique-location-churn benchmark is not initialized'),
+ )
+
+function warnDevMode(framework: Framework) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`,
+ )
+ }
+}
+
+export function createSetup(framework: Framework, mountTestApp: MountTestApp) {
+ warnDevMode(framework)
+
+ let container: HTMLDivElement | undefined = undefined
+ let unmount: (() => void) | undefined = undefined
+ let unsub = () => {}
+ let resolveRendered: () => void = () => {}
+ let navigateTo: (location: ItemLocation) => Promise = uninitialized
+
+ function assertRenderedId(expected: string) {
+ const actual =
+ container?.querySelector('[data-bench-id]')?.dataset.benchId
+
+ if (actual !== expected) {
+ throw new Error(`Expected rendered item id ${expected}, got ${actual}`)
+ }
+ }
+
+ async function waitForRenderedId(expected: string) {
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ assertRenderedId(expected)
+ return
+ } catch {
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve())
+ })
+ }
+ }
+
+ assertRenderedId(expected)
+ }
+
+ function waitForNextRender() {
+ return new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ }
+
+ async function before() {
+ if (container) {
+ after()
+ }
+
+ container = document.createElement('div')
+ document.body.append(container)
+
+ const mounted = mountTestApp(container)
+ const router = mounted.router as NavigationRouter
+ unmount = mounted.unmount
+
+ unsub = router.subscribe('onRendered', () => {
+ resolveRendered()
+ })
+
+ navigateTo = async (location) => {
+ const rendered = waitForNextRender()
+ await Promise.all([
+ router.navigate({
+ to: '/items/$id',
+ params: { id: location.id },
+ search: { q: location.q },
+ replace: true,
+ }),
+ rendered,
+ ])
+ }
+
+ await router.load()
+ await waitForRenderedId('initial')
+ }
+
+ function after() {
+ unmount?.()
+ container?.remove()
+ unsub()
+
+ container = undefined
+ unmount = undefined
+ unsub = () => {}
+ resolveRendered = () => {}
+ navigateTo = uninitialized
+ }
+
+ return {
+ name: `mem unique-location-churn (${framework})`,
+ before,
+ navigate: (location: ItemLocation) => navigateTo(location),
+ async run() {
+ for (let index = 0; index < uniqueLocationChurnIterations; index++) {
+ const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`
+ const q = `q-${randomSegment(benchmarkRandom)}`
+
+ await navigateTo({ id, q })
+ }
+ },
+ async sanity() {
+ await before()
+
+ try {
+ await navigateTo({ id: 'sanity-one', q: 'q-sanity-one' })
+ assertRenderedId('sanity-one')
+ await navigateTo({ id: 'sanity-two', q: 'q-sanity-two' })
+ assertRenderedId('sanity-two')
+ } finally {
+ after()
+ }
+ },
+ after,
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json
new file mode 100644
index 0000000000..5be22de7a4
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-unique-location-churn-solid",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts
new file mode 100644
index 0000000000..73e5f8949e
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('solid', mountTestApp)
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx
new file mode 100644
index 0000000000..16000c90a1
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx
@@ -0,0 +1,29 @@
+import { RouterProvider } from '@tanstack/solid-router'
+import { render } from 'solid-js/web'
+import { getRouter } from './router'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const dispose = render(() => , container)
+ let didUnmount = false
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ dispose()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..4a451735ff
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts
@@ -0,0 +1,59 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/items/$id'
+ id: '__root__' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx
new file mode 100644
index 0000000000..2969b06914
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/items/initial?q=q-initial'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..cb8d5a688d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx
new file mode 100644
index 0000000000..9724922b8d
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+type ItemSearch = {
+ q: string
+}
+
+export const Route = createFileRoute('/items/$id')({
+ validateSearch: (search: Record): ItemSearch => ({
+ q: typeof search.q === 'string' ? search.q : '',
+ }),
+ loaderDeps: ({ search }) => ({ q: search.q }),
+ loader: ({ params, deps }) => ({
+ id: params.id,
+ q: deps.q,
+ checksum: params.id.length + deps.q.length,
+ }),
+ component: ItemComponent,
+})
+
+function ItemComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+ {`${data().id}:${data().q}:${data().checksum}`}
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json
new file mode 100644
index 0000000000..b12dcb7ade
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts
new file mode 100644
index 0000000000..d4acf0019a
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ solid({ hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client unique-location-churn (solid)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts
new file mode 100644
index 0000000000..5b1c779d71
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts
@@ -0,0 +1,9 @@
+import { describe } from 'vitest'
+import { registerClientMemoryBench } from '#memory-client/runner'
+import { setup } from './setup'
+
+await setup().sanity()
+
+describe('memory', () => {
+ registerClientMemoryBench(setup())
+})
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts
new file mode 100644
index 0000000000..b464c772b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runClientFlameBenchmark } from '#memory-client/flame-runner'
+import { setup } from './setup.ts'
+
+await runClientFlameBenchmark(setup)
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json
new file mode 100644
index 0000000000..6c05cf80b0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-client-unique-location-churn-vue",
+ "projectType": "application",
+ "targets": {
+ "build:client": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:client:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:client:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:client": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-router"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts
new file mode 100644
index 0000000000..0f76d289d4
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts
@@ -0,0 +1,11 @@
+import type * as App from './src/app'
+import { createSetup } from '../shared'
+
+const appModulePath = './dist/app.js'
+const { mountTestApp } = (await import(
+ /* @vite-ignore */ appModulePath
+)) as typeof App
+
+export function setup() {
+ return createSetup('vue', mountTestApp)
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx
new file mode 100644
index 0000000000..fad2251cc0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx
@@ -0,0 +1,36 @@
+import { RouterProvider } from '@tanstack/vue-router'
+import { createApp } from 'vue'
+import { getRouter } from './router'
+import type {} from '@tanstack/router-core'
+
+export function mountTestApp(container: Element) {
+ const router = getRouter()
+ const vueApp = createApp({
+ setup() {
+ return () =>
+ },
+ })
+ let didUnmount = false
+
+ vueApp.mount(container)
+
+ // Full teardown mirrors the mount-unmount scenario: guard double-unmounts,
+ // release the devtools global, and detach history listeners.
+ return {
+ router,
+ unmount() {
+ if (didUnmount) {
+ return
+ }
+
+ didUnmount = true
+ vueApp.unmount()
+
+ if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) {
+ self.__TSR_ROUTER__ = undefined
+ }
+
+ router.history.destroy()
+ },
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..d3e9db0345
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts
@@ -0,0 +1,59 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/items/$id'
+ id: '__root__' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx
new file mode 100644
index 0000000000..72f9ab565b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx
@@ -0,0 +1,17 @@
+import { createMemoryHistory, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/items/initial?q=q-initial'],
+ }),
+ routeTree,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..91296e6f84
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx
new file mode 100644
index 0000000000..1854367142
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx
@@ -0,0 +1,34 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+type ItemSearch = {
+ q: string
+}
+
+export const Route = createFileRoute('/items/$id')({
+ validateSearch: (search: Record): ItemSearch => ({
+ q: typeof search.q === 'string' ? search.q : '',
+ }),
+ loaderDeps: ({ search }: { search: ItemSearch }) => ({ q: search.q }),
+ loader: ({
+ params,
+ deps,
+ }: {
+ params: { id: string }
+ deps: { q: string }
+ }) => ({
+ id: params.id,
+ q: deps.q,
+ checksum: params.id.length + deps.q.length,
+ }),
+ component: ItemComponent,
+})
+
+function ItemComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+ {`${data.value.id}:${data.value.q}:${data.value.checksum}`}
+ )
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json
new file mode 100644
index 0000000000..9a5872a4c0
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowImportingTsExtensions": true,
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "../../../vitest.setup.ts"
+ ]
+}
diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts
new file mode 100644
index 0000000000..423126019b
--- /dev/null
+++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts
@@ -0,0 +1,36 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ vue(),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ lib: {
+ entry: './src/app.tsx',
+ formats: ['es'],
+ fileName: 'app',
+ },
+ },
+ test: {
+ name: '@benchmarks/memory-client unique-location-churn (vue)',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['../../../vitest.setup.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/vitest.solid.config.ts b/benchmarks/memory/client/vitest.solid.config.ts
new file mode 100644
index 0000000000..5c8185cdd9
--- /dev/null
+++ b/benchmarks/memory/client/vitest.solid.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ watch: false,
+ fileParallelism: false,
+ projects: ['./scenarios/*/solid/vite.config.ts'],
+ },
+})
diff --git a/benchmarks/memory/client/vitest.vue.config.ts b/benchmarks/memory/client/vitest.vue.config.ts
new file mode 100644
index 0000000000..01768185ee
--- /dev/null
+++ b/benchmarks/memory/client/vitest.vue.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ watch: false,
+ fileParallelism: false,
+ projects: ['./scenarios/*/vue/vite.config.ts'],
+ },
+})
diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json
index 2427549e87..d898755beb 100644
--- a/benchmarks/memory/server/package.json
+++ b/benchmarks/memory/server/package.json
@@ -13,16 +13,24 @@
"dependencies": {
"@tanstack/react-router": "workspace:*",
"@tanstack/react-start": "workspace:*",
+ "@tanstack/solid-router": "workspace:*",
+ "@tanstack/solid-start": "workspace:*",
+ "@tanstack/vue-router": "workspace:*",
+ "@tanstack/vue-start": "workspace:*",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "solid-js": "^1.9.10",
+ "vue": "^3.5.16"
},
"devDependencies": {
"@codspeed/vitest-plugin": "^5.5.0",
"@datadog/pprof": "^5.13.2",
"@platformatic/flame": "^1.6.0",
"@vitejs/plugin-react": "^6.0.1",
+ "@vitejs/plugin-vue-jsx": "^5.1.5",
"typescript": "^6.0.2",
"vite": "^8.0.14",
+ "vite-plugin-solid": "^2.11.11",
"vitest": "^4.1.4"
},
"nx": {
@@ -45,6 +53,42 @@
}
]
},
+ "build:solid": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-solid",
+ "@benchmarks/memory-server-server-fn-churn-solid",
+ "@benchmarks/memory-server-error-paths-solid",
+ "@benchmarks/memory-server-aborted-requests-solid",
+ "@benchmarks/memory-server-peak-large-page-solid",
+ "@benchmarks/memory-server-streaming-peak-solid",
+ "@benchmarks/memory-server-serialization-payload-solid"
+ ],
+ "target": "build:ssr"
+ }
+ ]
+ },
+ "build:vue": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-vue",
+ "@benchmarks/memory-server-server-fn-churn-vue",
+ "@benchmarks/memory-server-error-paths-vue",
+ "@benchmarks/memory-server-aborted-requests-vue",
+ "@benchmarks/memory-server-peak-large-page-vue",
+ "@benchmarks/memory-server-streaming-peak-vue",
+ "@benchmarks/memory-server-serialization-payload-vue"
+ ],
+ "target": "build:ssr"
+ }
+ ]
+ },
"build:react:flame": {
"executor": "nx:noop",
"cache": false,
@@ -63,6 +107,42 @@
}
]
},
+ "build:solid:flame": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-solid",
+ "@benchmarks/memory-server-server-fn-churn-solid",
+ "@benchmarks/memory-server-error-paths-solid",
+ "@benchmarks/memory-server-aborted-requests-solid",
+ "@benchmarks/memory-server-peak-large-page-solid",
+ "@benchmarks/memory-server-streaming-peak-solid",
+ "@benchmarks/memory-server-serialization-payload-solid"
+ ],
+ "target": "build:ssr:flame"
+ }
+ ]
+ },
+ "build:vue:flame": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-vue",
+ "@benchmarks/memory-server-server-fn-churn-vue",
+ "@benchmarks/memory-server-error-paths-vue",
+ "@benchmarks/memory-server-aborted-requests-vue",
+ "@benchmarks/memory-server-peak-large-page-vue",
+ "@benchmarks/memory-server-streaming-peak-vue",
+ "@benchmarks/memory-server-serialization-payload-vue"
+ ],
+ "target": "build:ssr:flame"
+ }
+ ]
+ },
"test:flame:react": {
"executor": "nx:noop",
"cache": false,
@@ -81,6 +161,42 @@
}
]
},
+ "test:flame:solid": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-solid",
+ "@benchmarks/memory-server-server-fn-churn-solid",
+ "@benchmarks/memory-server-error-paths-solid",
+ "@benchmarks/memory-server-aborted-requests-solid",
+ "@benchmarks/memory-server-peak-large-page-solid",
+ "@benchmarks/memory-server-streaming-peak-solid",
+ "@benchmarks/memory-server-serialization-payload-solid"
+ ],
+ "target": "test:flame"
+ }
+ ]
+ },
+ "test:flame:vue": {
+ "executor": "nx:noop",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": [
+ "@benchmarks/memory-server-request-churn-vue",
+ "@benchmarks/memory-server-server-fn-churn-vue",
+ "@benchmarks/memory-server-error-paths-vue",
+ "@benchmarks/memory-server-aborted-requests-vue",
+ "@benchmarks/memory-server-peak-large-page-vue",
+ "@benchmarks/memory-server-streaming-peak-vue",
+ "@benchmarks/memory-server-serialization-payload-vue"
+ ],
+ "target": "test:flame"
+ }
+ ]
+ },
"test:perf:react": {
"executor": "nx:run-commands",
"parallelism": false,
@@ -93,6 +209,30 @@
"cwd": "benchmarks/memory/server"
}
},
+ "test:perf:solid": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": [
+ "build:solid"
+ ],
+ "options": {
+ "command": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts",
+ "cwd": "benchmarks/memory/server"
+ }
+ },
+ "test:perf:vue": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": [
+ "build:vue"
+ ],
+ "options": {
+ "command": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts",
+ "cwd": "benchmarks/memory/server"
+ }
+ },
"test:types": {
"executor": "nx:noop",
"dependsOn": [
@@ -104,7 +244,21 @@
"@benchmarks/memory-server-aborted-requests-react",
"@benchmarks/memory-server-peak-large-page-react",
"@benchmarks/memory-server-streaming-peak-react",
- "@benchmarks/memory-server-serialization-payload-react"
+ "@benchmarks/memory-server-serialization-payload-react",
+ "@benchmarks/memory-server-request-churn-solid",
+ "@benchmarks/memory-server-server-fn-churn-solid",
+ "@benchmarks/memory-server-error-paths-solid",
+ "@benchmarks/memory-server-aborted-requests-solid",
+ "@benchmarks/memory-server-peak-large-page-solid",
+ "@benchmarks/memory-server-streaming-peak-solid",
+ "@benchmarks/memory-server-serialization-payload-solid",
+ "@benchmarks/memory-server-request-churn-vue",
+ "@benchmarks/memory-server-server-fn-churn-vue",
+ "@benchmarks/memory-server-error-paths-vue",
+ "@benchmarks/memory-server-aborted-requests-vue",
+ "@benchmarks/memory-server-peak-large-page-vue",
+ "@benchmarks/memory-server-streaming-peak-vue",
+ "@benchmarks/memory-server-serialization-payload-vue"
],
"target": "test:types:ssr"
}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
index b3f1fc047b..66aed269b9 100644
--- a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts
@@ -1,147 +1,7 @@
-import type { StartRequestHandler } from '#memory-server/bench-utils'
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const abortedRequestIterations = 100
-let abortedRequestCounter = 0
-const eagerMarker = 'data-bench="aborted-requests-eager"'
-const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"'
-const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"'
-const alphaFirstRecord = (id: string) => `deferred-alpha-${id}-0`
-const alphaLastRecord = (id: string) => `deferred-alpha-${id}-19`
-const betaFirstRecord = (id: string) => `deferred-beta-${id}-0`
-const betaLastRecord = (id: string) => `deferred-beta-${id}-19`
-
-const textDecoder = new TextDecoder()
-const documentRequestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-function buildStreamRequest(id: string, signal?: AbortSignal) {
- const init: RequestInit = { ...documentRequestInit }
-
- if (signal) {
- init.signal = signal
- }
-
- return new Request(`http://localhost/stream/${id}`, init)
-}
-
-function validateDocumentResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-async function readFirstChunk(response: Response, request: Request) {
- if (!response.body) {
- throw new Error(`Expected response body for ${request.url}`)
- }
-
- const reader = response.body.getReader()
- const result = await reader.read()
- const value = result.value
-
- if (result.done || !value || value.byteLength === 0) {
- await reader.cancel()
- throw new Error(`Expected a non-empty first chunk for ${request.url}`)
- }
-
- return {
- reader,
- value,
- }
-}
-
-async function drainCancellationMicrotasks() {
- await Promise.resolve()
- await Promise.resolve()
-}
-
-async function assertAbortedRequestsSanity(handler: StartRequestHandler) {
- const fullId = 'sanity-full'
- const fullRequest = buildStreamRequest(fullId)
- const fullResponse = await handler.fetch(fullRequest)
- validateDocumentResponse(fullResponse, fullRequest)
-
- const fullBody = await fullResponse.text()
-
- if (!fullBody.includes(eagerMarker)) {
- throw new Error('Expected full sanity response to include the eager marker')
- }
-
- for (const marker of [
- alphaFirstRecord(fullId),
- alphaLastRecord(fullId),
- betaFirstRecord(fullId),
- betaLastRecord(fullId),
- ]) {
- if (!fullBody.includes(marker)) {
- throw new Error(`Expected full sanity response to include ${marker}`)
- }
- }
-
- const midStreamId = 'sanity-mid-stream'
- const controller = new AbortController()
- const midStreamRequest = buildStreamRequest(midStreamId, controller.signal)
- const midStreamResponse = await handler.fetch(midStreamRequest)
- validateDocumentResponse(midStreamResponse, midStreamRequest)
-
- const { reader, value } = await readFirstChunk(
- midStreamResponse,
- midStreamRequest,
- )
- const text = textDecoder.decode(value)
-
- if (!text.includes(eagerMarker)) {
- throw new Error('Expected first sanity chunk to include the eager marker')
- }
-
- if (
- !text.includes(alphaFallbackMarker) ||
- !text.includes(betaFallbackMarker)
- ) {
- throw new Error('Expected first sanity chunk to include deferred fallbacks')
- }
-
- for (const marker of [
- alphaFirstRecord(midStreamId),
- alphaLastRecord(midStreamId),
- betaFirstRecord(midStreamId),
- betaLastRecord(midStreamId),
- ]) {
- if (text.includes(marker)) {
- throw new Error(
- `First sanity chunk already included deferred content ${marker}`,
- )
- }
- }
-
- // reader.cancel() is the response-stream cancellation path if the handler
- // does not observe Request.signal for this in-process request.
- controller.abort()
- await reader.cancel()
- await drainCancellationMicrotasks()
-}
-
-async function runAbortedRequestLoop(handler: StartRequestHandler) {
- for (let index = 0; index < abortedRequestIterations; index++) {
- const controller = new AbortController()
- const id = `abort-${(abortedRequestCounter++).toString(36)}`
- const request = buildStreamRequest(id, controller.signal)
- const response = await handler.fetch(request)
- validateDocumentResponse(response, request)
-
- const { reader } = await readFirstChunk(response, request)
- controller.abort()
- await reader.cancel()
- await drainCancellationMicrotasks()
- }
-}
export async function setup() {
const { default: handler } = (await import(
@@ -150,15 +10,5 @@ export async function setup() {
default: StartRequestHandler
}
- const run = () => runAbortedRequestLoop(handler)
-
- return {
- sanity: () => assertAbortedRequestsSanity(handler),
- benches: [
- {
- name: 'mem aborted-requests (react)',
- run,
- },
- ],
- }
+ return createSetup('react', handler)
}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts
new file mode 100644
index 0000000000..efdfd6e139
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts
@@ -0,0 +1,309 @@
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+export type { StartRequestHandler }
+
+type Framework = 'react' | 'solid' | 'vue'
+
+type AbortedRequestReadMode = 'first-chunk' | 'shell-before-deferred'
+type AbortedRequestCancelMode = 'plain' | 'swallow-abort-error'
+type AbortedRequestDrainMode = 'microtasks' | 'tasks'
+
+type AbortedRequestMode = {
+ readMode: AbortedRequestReadMode
+ cancelMode: AbortedRequestCancelMode
+ drainMode: AbortedRequestDrainMode
+}
+
+const abortedRequestIterations = 100
+let abortedRequestCounter = 0
+const eagerMarker = 'data-bench="aborted-requests-eager"'
+const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"'
+const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"'
+const alphaFirstRecord = (id: string) => `deferred-alpha-${id}-0`
+const alphaLastRecord = (id: string) => `deferred-alpha-${id}-19`
+const betaFirstRecord = (id: string) => `deferred-beta-${id}-0`
+const betaLastRecord = (id: string) => `deferred-beta-${id}-19`
+
+const textDecoder = new TextDecoder()
+const abortedRequestModes: Record = {
+ react: {
+ readMode: 'first-chunk',
+ cancelMode: 'plain',
+ drainMode: 'microtasks',
+ },
+ solid: {
+ readMode: 'first-chunk',
+ cancelMode: 'plain',
+ drainMode: 'tasks',
+ },
+ vue: {
+ readMode: 'shell-before-deferred',
+ cancelMode: 'swallow-abort-error',
+ drainMode: 'tasks',
+ },
+}
+
+const documentRequestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildStreamRequest(id: string, signal?: AbortSignal) {
+ const init: RequestInit = { ...documentRequestInit }
+
+ if (signal) {
+ init.signal = signal
+ }
+
+ return new Request(`http://localhost/stream/${id}`, init)
+}
+
+function validateDocumentResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+async function readFirstChunk(response: Response, request: Request) {
+ if (!response.body) {
+ throw new Error(`Expected response body for ${request.url}`)
+ }
+
+ const reader = response.body.getReader()
+ const result = await reader.read()
+ const value = result.value
+
+ if (result.done || !value || value.byteLength === 0) {
+ await reader.cancel()
+ throw new Error(`Expected a non-empty first chunk for ${request.url}`)
+ }
+
+ return {
+ reader,
+ value,
+ }
+}
+
+async function readShellBeforeDeferred(
+ response: Response,
+ request: Request,
+ id: string,
+) {
+ if (!response.body) {
+ throw new Error(`Expected response body for ${request.url}`)
+ }
+
+ const reader = response.body.getReader()
+ let text = ''
+
+ while (true) {
+ const result = await reader.read()
+ const value = result.value
+
+ if (result.done || !value || value.byteLength === 0) {
+ await reader.cancel()
+ throw new Error(`Expected shell content for ${request.url}`)
+ }
+
+ text += textDecoder.decode(value, { stream: true })
+
+ for (const marker of [
+ alphaFirstRecord(id),
+ alphaLastRecord(id),
+ betaFirstRecord(id),
+ betaLastRecord(id),
+ ]) {
+ if (text.includes(marker)) {
+ await reader.cancel()
+ throw new Error(
+ `Shell sanity chunks already included deferred content ${marker}`,
+ )
+ }
+ }
+
+ if (
+ text.includes(eagerMarker) &&
+ text.includes(alphaFallbackMarker) &&
+ text.includes(betaFallbackMarker)
+ ) {
+ return {
+ reader,
+ text,
+ }
+ }
+ }
+}
+
+async function readSanityStream(
+ mode: AbortedRequestReadMode,
+ response: Response,
+ request: Request,
+ id: string,
+) {
+ if (mode === 'shell-before-deferred') {
+ return readShellBeforeDeferred(response, request, id)
+ }
+
+ const { reader, value } = await readFirstChunk(response, request)
+
+ return {
+ reader,
+ text: textDecoder.decode(value),
+ }
+}
+
+function readLoopStream(
+ mode: AbortedRequestReadMode,
+ response: Response,
+ request: Request,
+ id: string,
+) {
+ if (mode === 'shell-before-deferred') {
+ return readShellBeforeDeferred(response, request, id)
+ }
+
+ return readFirstChunk(response, request)
+}
+
+async function cancelReader(
+ reader: ReadableStreamDefaultReader,
+ mode: AbortedRequestCancelMode,
+) {
+ if (mode === 'swallow-abort-error') {
+ try {
+ await reader.cancel()
+ } catch (error) {
+ if (!(error instanceof DOMException && error.name === 'AbortError')) {
+ throw error
+ }
+ }
+
+ return
+ }
+
+ await reader.cancel()
+}
+
+async function drainCancellation(mode: AbortedRequestDrainMode) {
+ await Promise.resolve()
+ await Promise.resolve()
+
+ if (mode === 'tasks') {
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ }
+}
+
+async function assertAbortedRequestsSanity(
+ handler: StartRequestHandler,
+ mode: AbortedRequestMode,
+) {
+ const fullId = 'sanity-full'
+ const fullRequest = buildStreamRequest(fullId)
+ const fullResponse = await handler.fetch(fullRequest)
+ validateDocumentResponse(fullResponse, fullRequest)
+
+ const fullBody = await fullResponse.text()
+
+ if (!fullBody.includes(eagerMarker)) {
+ throw new Error('Expected full sanity response to include the eager marker')
+ }
+
+ for (const marker of [
+ alphaFirstRecord(fullId),
+ alphaLastRecord(fullId),
+ betaFirstRecord(fullId),
+ betaLastRecord(fullId),
+ ]) {
+ if (!fullBody.includes(marker)) {
+ throw new Error(`Expected full sanity response to include ${marker}`)
+ }
+ }
+
+ const midStreamId = 'sanity-mid-stream'
+ const controller = new AbortController()
+ const midStreamRequest = buildStreamRequest(midStreamId, controller.signal)
+ const midStreamResponse = await handler.fetch(midStreamRequest)
+ validateDocumentResponse(midStreamResponse, midStreamRequest)
+
+ const { reader, text } = await readSanityStream(
+ mode.readMode,
+ midStreamResponse,
+ midStreamRequest,
+ midStreamId,
+ )
+
+ if (!text.includes(eagerMarker)) {
+ throw new Error('Expected first sanity chunk to include the eager marker')
+ }
+
+ if (
+ !text.includes(alphaFallbackMarker) ||
+ !text.includes(betaFallbackMarker)
+ ) {
+ throw new Error('Expected first sanity chunk to include deferred fallbacks')
+ }
+
+ for (const marker of [
+ alphaFirstRecord(midStreamId),
+ alphaLastRecord(midStreamId),
+ betaFirstRecord(midStreamId),
+ betaLastRecord(midStreamId),
+ ]) {
+ if (text.includes(marker)) {
+ throw new Error(
+ `First sanity chunk already included deferred content ${marker}`,
+ )
+ }
+ }
+
+ // reader.cancel() is the response-stream cancellation path if the handler
+ // does not observe Request.signal for this in-process request.
+ controller.abort()
+ await cancelReader(reader, mode.cancelMode)
+ await drainCancellation(mode.drainMode)
+}
+
+async function runAbortedRequestLoop(
+ handler: StartRequestHandler,
+ mode: AbortedRequestMode,
+) {
+ for (let index = 0; index < abortedRequestIterations; index++) {
+ const controller = new AbortController()
+ const id = `abort-${(abortedRequestCounter++).toString(36)}`
+ const request = buildStreamRequest(id, controller.signal)
+ const response = await handler.fetch(request)
+ validateDocumentResponse(response, request)
+
+ const { reader } = await readLoopStream(
+ mode.readMode,
+ response,
+ request,
+ id,
+ )
+ controller.abort()
+ await cancelReader(reader, mode.cancelMode)
+ await drainCancellation(mode.drainMode)
+ }
+}
+
+export function createSetup(
+ framework: Framework,
+ handler: StartRequestHandler,
+) {
+ const mode = abortedRequestModes[framework]
+ const run = () => runAbortedRequestLoop(handler, mode)
+
+ return {
+ sanity: () => assertAbortedRequestsSanity(handler, mode),
+ benches: [
+ {
+ name: `mem aborted-requests (${framework})`,
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts
new file mode 100644
index 0000000000..1dc2e0da86
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts
@@ -0,0 +1,10 @@
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
+
+const test = await setup()
+await test.sanity()
+
+describe('memory', () => {
+ registerServerMemoryBenches(test)
+})
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json b/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json
new file mode 100644
index 0000000000..345fcc0720
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-server-aborted-requests-solid",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts
new file mode 100644
index 0000000000..0a575854b5
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts
@@ -0,0 +1,14 @@
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ return createSetup('solid', handler)
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..04ebed80f0
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as StreamIdRouteImport } from './routes/stream.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const StreamIdRoute = StreamIdRouteImport.update({
+ id: '/stream/$id',
+ path: '/stream/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/stream/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/stream/$id'
+ id: '__root__' | '/' | '/stream/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StreamIdRoute: typeof StreamIdRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/stream/$id': {
+ id: '/stream/$id'
+ path: '/stream/$id'
+ fullPath: '/stream/$id'
+ preLoaderRoute: typeof StreamIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StreamIdRoute: StreamIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/solid-start'
+declare module '@tanstack/solid-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx
new file mode 100644
index 0000000000..038ec0ab5e
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..aaf7dfdd89
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx
@@ -0,0 +1,29 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { HydrationScript } from 'solid-js/web'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charset: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx
new file mode 100644
index 0000000000..3aac56e824
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return aborted-requests-index
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx
new file mode 100644
index 0000000000..0d723098c8
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx
@@ -0,0 +1,133 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { Show, Suspense, createResource } from 'solid-js'
+
+const recordCount = 20
+const alphaDelayMs = 50
+const betaDelayMs = 75
+const abortProbeAlphaDelayMs = 500
+const abortProbeBetaDelayMs = 750
+
+type RecordGroup = 'alpha' | 'beta'
+
+export interface DeferredRecord {
+ id: string
+ label: string
+}
+
+function isAbortProbeId(id: string) {
+ return id === 'sanity-mid-stream' || id.startsWith('abort-')
+}
+
+function getDelay(id: string, group: RecordGroup) {
+ if (isAbortProbeId(id)) {
+ return group === 'alpha' ? abortProbeAlphaDelayMs : abortProbeBetaDelayMs
+ }
+
+ return group === 'alpha' ? alphaDelayMs : betaDelayMs
+}
+
+function resolveAfterDelay(
+ delayMs: number,
+ signal: AbortSignal,
+ value: () => T,
+ abortedValue: () => T,
+) {
+ return new Promise((resolve) => {
+ if (signal.aborted) {
+ resolve(abortedValue())
+ return
+ }
+
+ const timeoutId = setTimeout(() => {
+ signal.removeEventListener('abort', onAbort)
+ resolve(value())
+ }, delayMs)
+
+ const onAbort = () => {
+ clearTimeout(timeoutId)
+ resolve(abortedValue())
+ }
+
+ signal.addEventListener('abort', onAbort, { once: true })
+ })
+}
+
+function makeDeferredRecords(
+ id: string,
+ group: RecordGroup,
+ signal: AbortSignal,
+) {
+ const delayMs = getDelay(id, group)
+
+ return resolveAfterDelay(
+ delayMs,
+ signal,
+ () => makeRecords(id, group),
+ () => [],
+ )
+}
+
+function makeRecords(id: string, group: RecordGroup): Array {
+ return Array.from({ length: recordCount }, (_, index) => ({
+ id: `${group}-${id}-${index}`,
+ label: `deferred-${group}-${id}-${index}`,
+ }))
+}
+
+export const Route = createFileRoute('/stream/$id')({
+ loader: ({ params, abortController }) => ({
+ eager: `eager-${params.id}`,
+ alpha: makeDeferredRecords(params.id, 'alpha', abortController.signal),
+ beta: makeDeferredRecords(params.id, 'beta', abortController.signal),
+ }),
+ component: StreamComponent,
+})
+
+function StreamComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {data().eager}
+ loading-alpha
+ }
+ >
+
+
+ loading-beta
+ }
+ >
+
+
+
+ )
+}
+
+function DeferredRecords(props: {
+ promise: Promise>
+ dataBench: string
+}) {
+ const [records] = createResource(() => props.promise, (promise) => promise)
+
+ return (
+
+ {(resolvedRecords) => (
+
+ {resolvedRecords().map((record) => (
+ - {record.label}
+ ))}
+
+ )}
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json
new file mode 100644
index 0000000000..4b61264e11
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts
new file mode 100644
index 0000000000..d3a374cab7
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ solid({ ssr: true, hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server aborted-requests (solid)',
+ watch: false,
+ environment: 'node',
+ server: {
+ deps: {
+ inline: [/@solidjs/, /@tanstack\/solid-store/],
+ },
+ },
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts
new file mode 100644
index 0000000000..1dc2e0da86
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts
@@ -0,0 +1,10 @@
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
+
+const test = await setup()
+await test.sanity()
+
+describe('memory', () => {
+ registerServerMemoryBenches(test)
+})
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json b/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json
new file mode 100644
index 0000000000..8ca7d84660
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-server-aborted-requests-vue",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts
new file mode 100644
index 0000000000..df2908aec9
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts
@@ -0,0 +1,14 @@
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ return createSetup('vue', handler)
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..192f1d6dc7
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as StreamIdRouteImport } from './routes/stream.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const StreamIdRoute = StreamIdRouteImport.update({
+ id: '/stream/$id',
+ path: '/stream/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/stream/$id': typeof StreamIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/stream/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/stream/$id'
+ id: '__root__' | '/' | '/stream/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StreamIdRoute: typeof StreamIdRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/stream/$id': {
+ id: '/stream/$id'
+ path: '/stream/$id'
+ fullPath: '/stream/$id'
+ preLoaderRoute: typeof StreamIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StreamIdRoute: StreamIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/vue-start'
+declare module '@tanstack/vue-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx
new file mode 100644
index 0000000000..4290e7cdd3
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..de29ee1612
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx
@@ -0,0 +1,29 @@
+import {
+ Body,
+ HeadContent,
+ Html,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx
new file mode 100644
index 0000000000..af662b29f7
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return aborted-requests-index
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx
new file mode 100644
index 0000000000..8b2676523d
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx
@@ -0,0 +1,130 @@
+import { Await, createFileRoute } from '@tanstack/vue-router'
+import { Suspense } from 'vue'
+
+const recordCount = 20
+const alphaDelayMs = 50
+const betaDelayMs = 75
+const abortProbeAlphaDelayMs = 500
+const abortProbeBetaDelayMs = 750
+
+type RecordGroup = 'alpha' | 'beta'
+
+export interface DeferredRecord {
+ id: string
+ label: string
+}
+
+function isAbortProbeId(id: string) {
+ return id === 'sanity-mid-stream' || id.startsWith('abort-')
+}
+
+function getDelay(id: string, group: RecordGroup) {
+ if (isAbortProbeId(id)) {
+ return group === 'alpha' ? abortProbeAlphaDelayMs : abortProbeBetaDelayMs
+ }
+
+ return group === 'alpha' ? alphaDelayMs : betaDelayMs
+}
+
+function resolveAfterDelay(
+ delayMs: number,
+ signal: AbortSignal,
+ value: () => T,
+ abortedValue: () => T,
+) {
+ return new Promise((resolve) => {
+ if (signal.aborted) {
+ resolve(abortedValue())
+ return
+ }
+
+ const timeoutId = setTimeout(() => {
+ signal.removeEventListener('abort', onAbort)
+ resolve(value())
+ }, delayMs)
+
+ const onAbort = () => {
+ clearTimeout(timeoutId)
+ resolve(abortedValue())
+ }
+
+ signal.addEventListener('abort', onAbort, { once: true })
+ })
+}
+
+function makeDeferredRecords(
+ id: string,
+ group: RecordGroup,
+ signal: AbortSignal,
+) {
+ const delayMs = getDelay(id, group)
+
+ return resolveAfterDelay(
+ delayMs,
+ signal,
+ () => makeRecords(id, group),
+ () => [],
+ )
+}
+
+function makeRecords(id: string, group: RecordGroup): Array {
+ return Array.from({ length: recordCount }, (_, index) => ({
+ id: `${group}-${id}-${index}`,
+ label: `deferred-${group}-${id}-${index}`,
+ }))
+}
+
+export const Route = createFileRoute('/stream/$id')({
+ loader: ({ params, abortController }) => ({
+ eager: `eager-${params.id}`,
+ alpha: makeDeferredRecords(params.id, 'alpha', abortController.signal),
+ beta: makeDeferredRecords(params.id, 'beta', abortController.signal),
+ }),
+ component: StreamComponent,
+})
+
+function StreamComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+
+ {data.value.eager}
+ loading-alpha
+ loading-beta
+
+ {{
+ default: () => (
+ ) => (
+
+ {records.map((record) => (
+ - {record.label}
+ ))}
+
+ )}
+ />
+ ),
+ fallback: () => null,
+ }}
+
+
+ {{
+ default: () => (
+ ) => (
+
+ {records.map((record) => (
+ - {record.label}
+ ))}
+
+ )}
+ />
+ ),
+ fallback: () => null,
+ }}
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json
new file mode 100644
index 0000000000..9ad6481342
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts
new file mode 100644
index 0000000000..2b42e2be39
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/vue-start/plugin/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server aborted-requests (vue)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
index 3e5b658ff6..66aed269b9 100644
--- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts
@@ -1,153 +1,7 @@
-import {
- createDeterministicRandom,
- randomSegment,
- runSequentialRequestLoop,
-} from '#memory-server/bench-utils'
-import type { StartRequestHandler } from '#memory-server/bench-utils'
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const errorPathsIterations = 50
-const redirectSeed = 0xdecafbad
-const notFoundSeed = 0xdecafb0d
-const errorSeed = 0xdecafbed
-const unmatchedSeed = 0xdecaf00d
-// Module-level so each error-path bench keeps advancing across runner invocations.
-const redirectRandom = createDeterministicRandom(redirectSeed)
-const notFoundRandom = createDeterministicRandom(notFoundSeed)
-const errorRandom = createDeterministicRandom(errorSeed)
-const unmatchedRandom = createDeterministicRandom(unmatchedSeed)
-let redirectCounter = 0
-let notFoundCounter = 0
-let errorCounter = 0
-let unmatchedCounter = 0
-
-const redirectStatus = 302
-const notFoundStatus = 404
-const errorStatus = 500
-const notFoundMarker = 'data-bench="not-found-boundary"'
-const errorMarker = 'data-bench="error-boundary"'
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-function buildRedirectRequest(random: () => number) {
- const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}`
-
- return new Request(`http://localhost/from/${id}`, requestInit)
-}
-
-function buildNotFoundRequest(random: () => number) {
- const id = `${(notFoundCounter++).toString(36)}-${randomSegment(random)}`
-
- return new Request(`http://localhost/missing/${id}`, requestInit)
-}
-
-function buildErrorRequest(random: () => number) {
- const id = `${(errorCounter++).toString(36)}-${randomSegment(random)}`
-
- return new Request(`http://localhost/boom/${id}`, requestInit)
-}
-
-function buildUnmatchedRequest(random: () => number) {
- const id = `${(unmatchedCounter++).toString(36)}-${randomSegment(random)}`
-
- return new Request(`http://localhost/nope/${id}`, requestInit)
-}
-
-function getRequestId(request: Request) {
- const id = new URL(request.url).pathname.split('/').pop()
-
- if (!id) {
- throw new Error(`Expected request id in ${request.url}`)
- }
-
- return id
-}
-
-function validateRedirectResponse(response: Response, request: Request) {
- if (response.status !== redirectStatus) {
- throw new Error(
- `Expected status ${redirectStatus} for ${request.url}, got ${response.status}`,
- )
- }
-
- const id = getRequestId(request)
- const location = response.headers.get('location')
-
- if (location !== `/target/${id}`) {
- throw new Error(`Expected redirect location /target/${id}, got ${location}`)
- }
-}
-
-function validateNotFoundResponse(response: Response, request: Request) {
- if (response.status !== notFoundStatus) {
- throw new Error(
- `Expected status ${notFoundStatus} for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validateErrorResponse(response: Response, request: Request) {
- if (response.status !== errorStatus) {
- throw new Error(
- `Expected status ${errorStatus} for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validateNotFoundBody(body: string) {
- if (!body.includes(notFoundMarker)) {
- throw new Error('Expected error-paths not-found marker in response body')
- }
-}
-
-function validateErrorBody(body: string) {
- if (!body.includes(errorMarker)) {
- throw new Error('Expected error-paths error marker in response body')
- }
-}
-
-async function assertStatusSanity(
- handler: StartRequestHandler,
- request: Request,
- validateResponse: (response: Response, request: Request) => void,
- validateBody?: (body: string) => void,
-) {
- const response = await handler.fetch(request)
- validateResponse(response, request)
-
- const body = await response.text()
- validateBody?.(body)
-}
-
-async function assertErrorPathsSanity(handler: StartRequestHandler) {
- await assertStatusSanity(
- handler,
- new Request('http://localhost/from/sanity-redirect', requestInit),
- validateRedirectResponse,
- )
- await assertStatusSanity(
- handler,
- new Request('http://localhost/missing/sanity-missing', requestInit),
- validateNotFoundResponse,
- validateNotFoundBody,
- )
- await assertStatusSanity(
- handler,
- new Request('http://localhost/boom/sanity-error', requestInit),
- validateErrorResponse,
- validateErrorBody,
- )
- await assertStatusSanity(
- handler,
- new Request('http://localhost/nope/sanity-unmatched', requestInit),
- validateNotFoundResponse,
- )
-}
export async function setup() {
const { default: handler } = (await import(
@@ -156,57 +10,5 @@ export async function setup() {
default: StartRequestHandler
}
- const runRedirect = () =>
- runSequentialRequestLoop(handler, {
- random: redirectRandom,
- iterations: errorPathsIterations,
- buildRequest: buildRedirectRequest,
- validateResponse: validateRedirectResponse,
- })
-
- const runNotFound = () =>
- runSequentialRequestLoop(handler, {
- random: notFoundRandom,
- iterations: errorPathsIterations,
- buildRequest: buildNotFoundRequest,
- validateResponse: validateNotFoundResponse,
- })
-
- const runError = () =>
- runSequentialRequestLoop(handler, {
- random: errorRandom,
- iterations: errorPathsIterations,
- buildRequest: buildErrorRequest,
- validateResponse: validateErrorResponse,
- })
-
- const runUnmatched = () =>
- runSequentialRequestLoop(handler, {
- random: unmatchedRandom,
- iterations: errorPathsIterations,
- buildRequest: buildUnmatchedRequest,
- validateResponse: validateNotFoundResponse,
- })
-
- return {
- sanity: () => assertErrorPathsSanity(handler),
- benches: [
- {
- name: 'mem error-paths redirect (react)',
- run: runRedirect,
- },
- {
- name: 'mem error-paths not-found (react)',
- run: runNotFound,
- },
- {
- name: 'mem error-paths error (react)',
- run: runError,
- },
- {
- name: 'mem error-paths unmatched (react)',
- run: runUnmatched,
- },
- ],
- }
+ return createSetup('react', handler)
}
diff --git a/benchmarks/memory/server/scenarios/error-paths/shared.ts b/benchmarks/memory/server/scenarios/error-paths/shared.ts
new file mode 100644
index 0000000000..f552d33f2a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/shared.ts
@@ -0,0 +1,211 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+ runSequentialRequestLoop,
+} from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+export type { StartRequestHandler }
+
+type Framework = 'react' | 'solid' | 'vue'
+
+const errorPathsIterations = 50
+const redirectSeed = 0xdecafbad
+const notFoundSeed = 0xdecafb0d
+const errorSeed = 0xdecafbed
+const unmatchedSeed = 0xdecaf00d
+const redirectStatus = 302
+const notFoundStatus = 404
+const errorStatus = 500
+const notFoundMarker = 'data-bench="not-found-boundary"'
+const errorMarker = 'data-bench="error-boundary"'
+// Module-level so each error-path bench keeps advancing across runner invocations.
+const redirectRandom = createDeterministicRandom(redirectSeed)
+const notFoundRandom = createDeterministicRandom(notFoundSeed)
+const errorRandom = createDeterministicRandom(errorSeed)
+const unmatchedRandom = createDeterministicRandom(unmatchedSeed)
+let redirectCounter = 0
+let notFoundCounter = 0
+let errorCounter = 0
+let unmatchedCounter = 0
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildRedirectRequest(random: () => number) {
+ const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}`
+
+ return new Request(`http://localhost/from/${id}`, requestInit)
+}
+
+function buildNotFoundRequest(random: () => number) {
+ const id = `${(notFoundCounter++).toString(36)}-${randomSegment(random)}`
+
+ return new Request(`http://localhost/missing/${id}`, requestInit)
+}
+
+function buildErrorRequest(random: () => number) {
+ const id = `${(errorCounter++).toString(36)}-${randomSegment(random)}`
+
+ return new Request(`http://localhost/boom/${id}`, requestInit)
+}
+
+function buildUnmatchedRequest(random: () => number) {
+ const id = `${(unmatchedCounter++).toString(36)}-${randomSegment(random)}`
+
+ return new Request(`http://localhost/nope/${id}`, requestInit)
+}
+
+function getRequestId(request: Request) {
+ const id = new URL(request.url).pathname.split('/').pop()
+
+ if (!id) {
+ throw new Error(`Expected request id in ${request.url}`)
+ }
+
+ return id
+}
+
+function validateRedirectResponse(response: Response, request: Request) {
+ if (response.status !== redirectStatus) {
+ throw new Error(
+ `Expected status ${redirectStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+
+ const id = getRequestId(request)
+ const location = response.headers.get('location')
+
+ if (location !== `/target/${id}`) {
+ throw new Error(`Expected redirect location /target/${id}, got ${location}`)
+ }
+}
+
+function validateNotFoundResponse(response: Response, request: Request) {
+ if (response.status !== notFoundStatus) {
+ throw new Error(
+ `Expected status ${notFoundStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateErrorResponse(response: Response, request: Request) {
+ if (response.status !== errorStatus) {
+ throw new Error(
+ `Expected status ${errorStatus} for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateNotFoundBody(body: string) {
+ if (!body.includes(notFoundMarker)) {
+ throw new Error('Expected error-paths not-found marker in response body')
+ }
+}
+
+function validateErrorBody(body: string) {
+ if (!body.includes(errorMarker)) {
+ throw new Error('Expected error-paths error marker in response body')
+ }
+}
+
+async function assertStatusSanity(
+ handler: StartRequestHandler,
+ request: Request,
+ validateResponse: (response: Response, request: Request) => void,
+ validateBody?: (body: string) => void,
+) {
+ const response = await handler.fetch(request)
+ validateResponse(response, request)
+
+ const body = await response.text()
+ validateBody?.(body)
+}
+
+async function assertErrorPathsSanity(handler: StartRequestHandler) {
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/from/sanity-redirect', requestInit),
+ validateRedirectResponse,
+ )
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/missing/sanity-missing', requestInit),
+ validateNotFoundResponse,
+ validateNotFoundBody,
+ )
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/boom/sanity-error', requestInit),
+ validateErrorResponse,
+ validateErrorBody,
+ )
+ await assertStatusSanity(
+ handler,
+ new Request('http://localhost/nope/sanity-unmatched', requestInit),
+ validateNotFoundResponse,
+ )
+}
+
+export function createSetup(
+ framework: Framework,
+ handler: StartRequestHandler,
+) {
+ const runRedirect = () =>
+ runSequentialRequestLoop(handler, {
+ random: redirectRandom,
+ iterations: errorPathsIterations,
+ buildRequest: buildRedirectRequest,
+ validateResponse: validateRedirectResponse,
+ })
+
+ const runNotFound = () =>
+ runSequentialRequestLoop(handler, {
+ random: notFoundRandom,
+ iterations: errorPathsIterations,
+ buildRequest: buildNotFoundRequest,
+ validateResponse: validateNotFoundResponse,
+ })
+
+ const runError = () =>
+ runSequentialRequestLoop(handler, {
+ random: errorRandom,
+ iterations: errorPathsIterations,
+ buildRequest: buildErrorRequest,
+ validateResponse: validateErrorResponse,
+ })
+
+ const runUnmatched = () =>
+ runSequentialRequestLoop(handler, {
+ random: unmatchedRandom,
+ iterations: errorPathsIterations,
+ buildRequest: buildUnmatchedRequest,
+ validateResponse: validateNotFoundResponse,
+ })
+
+ return {
+ sanity: () => assertErrorPathsSanity(handler),
+ benches: [
+ {
+ name: `mem error-paths redirect (${framework})`,
+ run: runRedirect,
+ },
+ {
+ name: `mem error-paths not-found (${framework})`,
+ run: runNotFound,
+ },
+ {
+ name: `mem error-paths error (${framework})`,
+ run: runError,
+ },
+ {
+ name: `mem error-paths unmatched (${framework})`,
+ run: runUnmatched,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts
new file mode 100644
index 0000000000..1dc2e0da86
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts
@@ -0,0 +1,10 @@
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
+
+const test = await setup()
+await test.sanity()
+
+describe('memory', () => {
+ registerServerMemoryBenches(test)
+})
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/project.json b/benchmarks/memory/server/scenarios/error-paths/solid/project.json
new file mode 100644
index 0000000000..77fcfaf578
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-server-error-paths-solid",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts
new file mode 100644
index 0000000000..0a575854b5
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts
@@ -0,0 +1,14 @@
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ return createSetup('solid', handler)
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..32a4d56d3a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts
@@ -0,0 +1,146 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as TargetIdRouteImport } from './routes/target.$id'
+import { Route as MissingIdRouteImport } from './routes/missing.$id'
+import { Route as FromIdRouteImport } from './routes/from.$id'
+import { Route as BoomIdRouteImport } from './routes/boom.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const TargetIdRoute = TargetIdRouteImport.update({
+ id: '/target/$id',
+ path: '/target/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MissingIdRoute = MissingIdRouteImport.update({
+ id: '/missing/$id',
+ path: '/missing/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FromIdRoute = FromIdRouteImport.update({
+ id: '/from/$id',
+ path: '/from/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const BoomIdRoute = BoomIdRouteImport.update({
+ id: '/boom/$id',
+ path: '/boom/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id'
+ id:
+ | '__root__'
+ | '/'
+ | '/boom/$id'
+ | '/from/$id'
+ | '/missing/$id'
+ | '/target/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ BoomIdRoute: typeof BoomIdRoute
+ FromIdRoute: typeof FromIdRoute
+ MissingIdRoute: typeof MissingIdRoute
+ TargetIdRoute: typeof TargetIdRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/target/$id': {
+ id: '/target/$id'
+ path: '/target/$id'
+ fullPath: '/target/$id'
+ preLoaderRoute: typeof TargetIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/missing/$id': {
+ id: '/missing/$id'
+ path: '/missing/$id'
+ fullPath: '/missing/$id'
+ preLoaderRoute: typeof MissingIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/from/$id': {
+ id: '/from/$id'
+ path: '/from/$id'
+ fullPath: '/from/$id'
+ preLoaderRoute: typeof FromIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/boom/$id': {
+ id: '/boom/$id'
+ path: '/boom/$id'
+ fullPath: '/boom/$id'
+ preLoaderRoute: typeof BoomIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ BoomIdRoute: BoomIdRoute,
+ FromIdRoute: FromIdRoute,
+ MissingIdRoute: MissingIdRoute,
+ TargetIdRoute: TargetIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/solid-start'
+declare module '@tanstack/solid-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx
new file mode 100644
index 0000000000..ae51f8b5e9
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx
@@ -0,0 +1,23 @@
+import { createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ defaultNotFoundComponent: DefaultNotFound,
+ })
+}
+
+// Without this, every unmatched-URL request logs a router warning in
+// non-production ad-hoc runs.
+function DefaultNotFound() {
+ return error-paths-unmatched
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..aaf7dfdd89
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx
@@ -0,0 +1,29 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { HydrationScript } from 'solid-js/web'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charset: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx
new file mode 100644
index 0000000000..bebc638caa
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/boom/$id')({
+ loader: ({ params }) => {
+ throw new Error(`boom-${params.id}`)
+ },
+ errorComponent: BoomErrorComponent,
+ component: BoomComponent,
+})
+
+function BoomErrorComponent() {
+ return error-paths-error-boundary
+}
+
+function BoomComponent() {
+ return null
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx
new file mode 100644
index 0000000000..e96b7864f2
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/from/$id')({
+ loader: ({ params }) => {
+ const { id } = params
+
+ throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 })
+ },
+ component: FromComponent,
+})
+
+function FromComponent() {
+ return null
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx
new file mode 100644
index 0000000000..43282af801
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return error-paths-index
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx
new file mode 100644
index 0000000000..1b9ec5e29d
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute, notFound } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/missing/$id')({
+ loader: () => {
+ throw notFound()
+ },
+ notFoundComponent: MissingNotFoundComponent,
+ component: MissingComponent,
+})
+
+function MissingNotFoundComponent() {
+ return (
+ error-paths-not-found-boundary
+ )
+}
+
+function MissingComponent() {
+ return null
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx
new file mode 100644
index 0000000000..4196fc1183
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/target/$id')({
+ component: TargetComponent,
+})
+
+function TargetComponent() {
+ const params = Route.useParams()
+
+ return {`target-${params().id}`}
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json
new file mode 100644
index 0000000000..4b61264e11
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts
new file mode 100644
index 0000000000..7f89abb099
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ solid({ ssr: true, hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server error-paths (solid)',
+ watch: false,
+ environment: 'node',
+ server: {
+ deps: {
+ inline: [/@solidjs/, /@tanstack\/solid-store/],
+ },
+ },
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts
new file mode 100644
index 0000000000..1dc2e0da86
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts
@@ -0,0 +1,10 @@
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
+
+const test = await setup()
+await test.sanity()
+
+describe('memory', () => {
+ registerServerMemoryBenches(test)
+})
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/project.json b/benchmarks/memory/server/scenarios/error-paths/vue/project.json
new file mode 100644
index 0000000000..8472470bde
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-server-error-paths-vue",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts
new file mode 100644
index 0000000000..df2908aec9
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts
@@ -0,0 +1,14 @@
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ return createSetup('vue', handler)
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..ea5bafa386
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts
@@ -0,0 +1,146 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as TargetIdRouteImport } from './routes/target.$id'
+import { Route as MissingIdRouteImport } from './routes/missing.$id'
+import { Route as FromIdRouteImport } from './routes/from.$id'
+import { Route as BoomIdRouteImport } from './routes/boom.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const TargetIdRoute = TargetIdRouteImport.update({
+ id: '/target/$id',
+ path: '/target/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MissingIdRoute = MissingIdRouteImport.update({
+ id: '/missing/$id',
+ path: '/missing/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FromIdRoute = FromIdRouteImport.update({
+ id: '/from/$id',
+ path: '/from/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const BoomIdRoute = BoomIdRouteImport.update({
+ id: '/boom/$id',
+ path: '/boom/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/boom/$id': typeof BoomIdRoute
+ '/from/$id': typeof FromIdRoute
+ '/missing/$id': typeof MissingIdRoute
+ '/target/$id': typeof TargetIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id'
+ id:
+ | '__root__'
+ | '/'
+ | '/boom/$id'
+ | '/from/$id'
+ | '/missing/$id'
+ | '/target/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ BoomIdRoute: typeof BoomIdRoute
+ FromIdRoute: typeof FromIdRoute
+ MissingIdRoute: typeof MissingIdRoute
+ TargetIdRoute: typeof TargetIdRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/target/$id': {
+ id: '/target/$id'
+ path: '/target/$id'
+ fullPath: '/target/$id'
+ preLoaderRoute: typeof TargetIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/missing/$id': {
+ id: '/missing/$id'
+ path: '/missing/$id'
+ fullPath: '/missing/$id'
+ preLoaderRoute: typeof MissingIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/from/$id': {
+ id: '/from/$id'
+ path: '/from/$id'
+ fullPath: '/from/$id'
+ preLoaderRoute: typeof FromIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/boom/$id': {
+ id: '/boom/$id'
+ path: '/boom/$id'
+ fullPath: '/boom/$id'
+ preLoaderRoute: typeof BoomIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ BoomIdRoute: BoomIdRoute,
+ FromIdRoute: FromIdRoute,
+ MissingIdRoute: MissingIdRoute,
+ TargetIdRoute: TargetIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/vue-start'
+declare module '@tanstack/vue-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx
new file mode 100644
index 0000000000..4290e7cdd3
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..de29ee1612
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx
@@ -0,0 +1,29 @@
+import {
+ Body,
+ HeadContent,
+ Html,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx
new file mode 100644
index 0000000000..d933b93df2
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/boom/$id')({
+ loader: ({ params }) => {
+ throw new Error(`boom-${params.id}`)
+ },
+ errorComponent: BoomErrorComponent,
+ component: BoomComponent,
+})
+
+function BoomErrorComponent() {
+ return error-paths-error-boundary
+}
+
+function BoomComponent() {
+ return <>>
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx
new file mode 100644
index 0000000000..6ffba96bce
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute, redirect } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/from/$id')({
+ loader: ({ params }) => {
+ const { id } = params
+
+ throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 })
+ },
+ component: FromComponent,
+})
+
+function FromComponent() {
+ return <>>
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx
new file mode 100644
index 0000000000..53fb604c60
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return error-paths-index
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx
new file mode 100644
index 0000000000..0e4972f5c6
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute, notFound } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/missing/$id')({
+ loader: () => {
+ throw notFound()
+ },
+ notFoundComponent: MissingNotFoundComponent,
+ component: MissingComponent,
+})
+
+function MissingNotFoundComponent() {
+ return (
+ error-paths-not-found-boundary
+ )
+}
+
+function MissingComponent() {
+ return <>>
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx
new file mode 100644
index 0000000000..17ade6bf5b
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/target/$id')({
+ component: TargetComponent,
+})
+
+function TargetComponent() {
+ const params = Route.useParams()
+
+ return {`target-${params.value.id}`}
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json
new file mode 100644
index 0000000000..9ad6481342
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts
new file mode 100644
index 0000000000..b5cf79924a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/vue-start/plugin/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server error-paths (vue)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
index 28936ea0c6..66aed269b9 100644
--- a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts
@@ -1,52 +1,7 @@
-import { runSequentialRequestLoop } from '#memory-server/bench-utils'
-import type { StartRequestHandler } from '#memory-server/bench-utils'
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const benchmarkSeed = 0x5eed_0005
-const peakLargePageIterations = 20
-const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8'
-const levelEightMarker = 'data-bench="peak-large-page-level-8"'
-const knownDehydratedRecordName = 'peak-large-page-l8-record-199'
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-function buildPeakLargePageRequest() {
- return new Request(peakLargePageUrl, requestInit)
-}
-
-function validatePeakLargePageResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validatePeakLargePageBody(body: string) {
- if (!body.includes(levelEightMarker)) {
- throw new Error('Expected peak-large-page level-8 marker in response body')
- }
-
- if (!body.includes(knownDehydratedRecordName)) {
- throw new Error(
- 'Expected peak-large-page dehydrated record in response body',
- )
- }
-}
-
-async function assertPeakLargePageSanity(handler: StartRequestHandler) {
- const request = new Request(peakLargePageUrl, requestInit)
- const response = await handler.fetch(request)
- const body = await response.text()
-
- validatePeakLargePageResponse(response, request)
- validatePeakLargePageBody(body)
-}
export async function setup() {
const { default: handler } = (await import(
@@ -55,21 +10,5 @@ export async function setup() {
default: StartRequestHandler
}
- const run = () =>
- runSequentialRequestLoop(handler, {
- seed: benchmarkSeed,
- iterations: peakLargePageIterations,
- buildRequest: buildPeakLargePageRequest,
- validateResponse: validatePeakLargePageResponse,
- })
-
- return {
- sanity: () => assertPeakLargePageSanity(handler),
- benches: [
- {
- name: 'mem peak-large-page (react)',
- run,
- },
- ],
- }
+ return createSetup('react', handler)
}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/shared.ts b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts
new file mode 100644
index 0000000000..1d020fed22
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts
@@ -0,0 +1,75 @@
+import { runSequentialRequestLoop } from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+export type { StartRequestHandler }
+
+type Framework = 'react' | 'solid' | 'vue'
+
+const benchmarkSeed = 0x5eed_0005
+const peakLargePageIterations = 20
+const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8'
+const levelEightMarker = 'data-bench="peak-large-page-level-8"'
+const knownDehydratedRecordName = 'peak-large-page-l8-record-199'
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function buildPeakLargePageRequest() {
+ return new Request(peakLargePageUrl, requestInit)
+}
+
+function validatePeakLargePageResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validatePeakLargePageBody(body: string) {
+ if (!body.includes(levelEightMarker)) {
+ throw new Error('Expected peak-large-page level-8 marker in response body')
+ }
+
+ if (!body.includes(knownDehydratedRecordName)) {
+ throw new Error(
+ 'Expected peak-large-page dehydrated record in response body',
+ )
+ }
+}
+
+async function assertPeakLargePageSanity(handler: StartRequestHandler) {
+ const request = new Request(peakLargePageUrl, requestInit)
+ const response = await handler.fetch(request)
+ const body = await response.text()
+
+ validatePeakLargePageResponse(response, request)
+ validatePeakLargePageBody(body)
+}
+
+export function createSetup(
+ framework: Framework,
+ handler: StartRequestHandler,
+) {
+ const run = () =>
+ runSequentialRequestLoop(handler, {
+ seed: benchmarkSeed,
+ iterations: peakLargePageIterations,
+ buildRequest: buildPeakLargePageRequest,
+ validateResponse: validatePeakLargePageResponse,
+ })
+
+ return {
+ sanity: () => assertPeakLargePageSanity(handler),
+ benches: [
+ {
+ name: `mem peak-large-page (${framework})`,
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts
new file mode 100644
index 0000000000..1dc2e0da86
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts
@@ -0,0 +1,10 @@
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
+
+const test = await setup()
+await test.sanity()
+
+describe('memory', () => {
+ registerServerMemoryBenches(test)
+})
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json b/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json
new file mode 100644
index 0000000000..85e8f83224
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-server-peak-large-page-solid",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts
new file mode 100644
index 0000000000..0a575854b5
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts
@@ -0,0 +1,14 @@
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ return createSetup('solid', handler)
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts
new file mode 100644
index 0000000000..8f77c58731
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts
@@ -0,0 +1,111 @@
+export interface LargePageRecord {
+ id: string
+ name: string
+ description: string
+}
+
+export interface LargePageLevelData {
+ level: number
+ marker: string
+ records: Array
+}
+
+const recordCount = 200
+const descriptionLength = 104
+
+export function makeLargePageLevelData(level: number, seed: number) {
+ const random = createDeterministicRandom(seed)
+
+ return {
+ level,
+ marker: makeLargePageMarker(level),
+ records: Array.from({ length: recordCount }, (_, index) => {
+ const idToken = randomSegment(random)
+ const descriptionTokenA = randomSegment(random)
+ const descriptionTokenB = randomSegment(random)
+
+ return {
+ id: `l${level}-${index}-${idToken}`,
+ name: makeLargePageRecordName(level, index),
+ description: makeDescription(
+ level,
+ index,
+ descriptionTokenA,
+ descriptionTokenB,
+ ),
+ }
+ }),
+ } satisfies LargePageLevelData
+}
+
+export function makeLargePageHead(loaderData: LargePageLevelData | undefined) {
+ if (!loaderData) {
+ return {
+ meta: [{ title: 'Peak Large Page' }],
+ }
+ }
+
+ const first = loaderData.records[0]!
+ const last = loaderData.records[loaderData.records.length - 1]!
+
+ return {
+ meta: [
+ { title: `Peak Large Page L${loaderData.level} ${first.name}` },
+ {
+ name: `peak-large-page-l${loaderData.level}-count`,
+ content: String(loaderData.records.length),
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-first-id`,
+ content: first.id,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-first-name`,
+ content: first.name,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-last-id`,
+ content: last.id,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-description`,
+ content: first.description,
+ },
+ ],
+ }
+}
+
+function makeLargePageRecordName(level: number, index: number) {
+ return `peak-large-page-l${level}-record-${index}`
+}
+
+function makeLargePageMarker(level: number) {
+ return `peak-large-page-level-${level}`
+}
+
+function makeDescription(
+ level: number,
+ index: number,
+ tokenA: string,
+ tokenB: string,
+) {
+ return `Level ${level} record ${index} uses seeded fragments ${tokenA} and ${tokenB} to keep a fresh deterministic loader payload.`.padEnd(
+ descriptionLength,
+ 'x',
+ )
+}
+
+// Local copy of the LCG from benchmarks/memory/server/bench-utils.ts - app
+// source cannot import from outside the app root. Keep in sync.
+function createDeterministicRandom(seed: number) {
+ let state = seed >>> 0
+
+ return () => {
+ state = (state * 1664525 + 1013904223) >>> 0
+ return state / 0x100000000
+ }
+}
+
+function randomSegment(random: () => number) {
+ return Math.floor(random() * 1_000_000_000).toString(36)
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..c3ccdaaa0e
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts
@@ -0,0 +1,305 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as L1RouteImport } from './routes/l1'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as L1L2RouteImport } from './routes/l1.l2'
+import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3'
+import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4'
+import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5'
+import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6'
+import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7'
+import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8'
+
+const L1Route = L1RouteImport.update({
+ id: '/l1',
+ path: '/l1',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const L1L2Route = L1L2RouteImport.update({
+ id: '/l2',
+ path: '/l2',
+ getParentRoute: () => L1Route,
+} as any)
+const L1L2L3Route = L1L2L3RouteImport.update({
+ id: '/l3',
+ path: '/l3',
+ getParentRoute: () => L1L2Route,
+} as any)
+const L1L2L3L4Route = L1L2L3L4RouteImport.update({
+ id: '/l4',
+ path: '/l4',
+ getParentRoute: () => L1L2L3Route,
+} as any)
+const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({
+ id: '/l5',
+ path: '/l5',
+ getParentRoute: () => L1L2L3L4Route,
+} as any)
+const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({
+ id: '/l6',
+ path: '/l6',
+ getParentRoute: () => L1L2L3L4L5Route,
+} as any)
+const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({
+ id: '/l7',
+ path: '/l7',
+ getParentRoute: () => L1L2L3L4L5L6Route,
+} as any)
+const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({
+ id: '/l8',
+ path: '/l8',
+ getParentRoute: () => L1L2L3L4L5L6L7Route,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ id:
+ | '__root__'
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ L1Route: typeof L1RouteWithChildren
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/l1': {
+ id: '/l1'
+ path: '/l1'
+ fullPath: '/l1'
+ preLoaderRoute: typeof L1RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/l1/l2': {
+ id: '/l1/l2'
+ path: '/l2'
+ fullPath: '/l1/l2'
+ preLoaderRoute: typeof L1L2RouteImport
+ parentRoute: typeof L1Route
+ }
+ '/l1/l2/l3': {
+ id: '/l1/l2/l3'
+ path: '/l3'
+ fullPath: '/l1/l2/l3'
+ preLoaderRoute: typeof L1L2L3RouteImport
+ parentRoute: typeof L1L2Route
+ }
+ '/l1/l2/l3/l4': {
+ id: '/l1/l2/l3/l4'
+ path: '/l4'
+ fullPath: '/l1/l2/l3/l4'
+ preLoaderRoute: typeof L1L2L3L4RouteImport
+ parentRoute: typeof L1L2L3Route
+ }
+ '/l1/l2/l3/l4/l5': {
+ id: '/l1/l2/l3/l4/l5'
+ path: '/l5'
+ fullPath: '/l1/l2/l3/l4/l5'
+ preLoaderRoute: typeof L1L2L3L4L5RouteImport
+ parentRoute: typeof L1L2L3L4Route
+ }
+ '/l1/l2/l3/l4/l5/l6': {
+ id: '/l1/l2/l3/l4/l5/l6'
+ path: '/l6'
+ fullPath: '/l1/l2/l3/l4/l5/l6'
+ preLoaderRoute: typeof L1L2L3L4L5L6RouteImport
+ parentRoute: typeof L1L2L3L4L5Route
+ }
+ '/l1/l2/l3/l4/l5/l6/l7': {
+ id: '/l1/l2/l3/l4/l5/l6/l7'
+ path: '/l7'
+ fullPath: '/l1/l2/l3/l4/l5/l6/l7'
+ preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport
+ parentRoute: typeof L1L2L3L4L5L6Route
+ }
+ '/l1/l2/l3/l4/l5/l6/l7/l8': {
+ id: '/l1/l2/l3/l4/l5/l6/l7/l8'
+ path: '/l8'
+ fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8'
+ preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport
+ parentRoute: typeof L1L2L3L4L5L6L7Route
+ }
+ }
+}
+
+interface L1L2L3L4L5L6L7RouteChildren {
+ L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route
+}
+
+const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = {
+ L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route,
+}
+
+const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren(
+ L1L2L3L4L5L6L7RouteChildren,
+)
+
+interface L1L2L3L4L5L6RouteChildren {
+ L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren
+}
+
+const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = {
+ L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren,
+}
+
+const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren(
+ L1L2L3L4L5L6RouteChildren,
+)
+
+interface L1L2L3L4L5RouteChildren {
+ L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren
+}
+
+const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = {
+ L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren,
+}
+
+const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren(
+ L1L2L3L4L5RouteChildren,
+)
+
+interface L1L2L3L4RouteChildren {
+ L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren
+}
+
+const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = {
+ L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren,
+}
+
+const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren(
+ L1L2L3L4RouteChildren,
+)
+
+interface L1L2L3RouteChildren {
+ L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren
+}
+
+const L1L2L3RouteChildren: L1L2L3RouteChildren = {
+ L1L2L3L4Route: L1L2L3L4RouteWithChildren,
+}
+
+const L1L2L3RouteWithChildren =
+ L1L2L3Route._addFileChildren(L1L2L3RouteChildren)
+
+interface L1L2RouteChildren {
+ L1L2L3Route: typeof L1L2L3RouteWithChildren
+}
+
+const L1L2RouteChildren: L1L2RouteChildren = {
+ L1L2L3Route: L1L2L3RouteWithChildren,
+}
+
+const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren)
+
+interface L1RouteChildren {
+ L1L2Route: typeof L1L2RouteWithChildren
+}
+
+const L1RouteChildren: L1RouteChildren = {
+ L1L2Route: L1L2RouteWithChildren,
+}
+
+const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ L1Route: L1RouteWithChildren,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/solid-start'
+declare module '@tanstack/solid-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx
new file mode 100644
index 0000000000..038ec0ab5e
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..aaf7dfdd89
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx
@@ -0,0 +1,29 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { HydrationScript } from 'solid-js/web'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charset: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx
new file mode 100644
index 0000000000..111ffaa36a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return peak-large-page-index
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
new file mode 100644
index 0000000000..6b8465a552
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({
+ loader: () => makeLargePageLevelData(8, 0x5eed_1008),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelEightComponent,
+})
+
+function LevelEightComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
new file mode 100644
index 0000000000..bdd3f6c71a
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({
+ loader: () => makeLargePageLevelData(7, 0x5eed_1007),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelSevenComponent,
+})
+
+function LevelSevenComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx
new file mode 100644
index 0000000000..6e3faefe63
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({
+ loader: () => makeLargePageLevelData(6, 0x5eed_1006),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelSixComponent,
+})
+
+function LevelSixComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx
new file mode 100644
index 0000000000..2a3ad3fa24
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5')({
+ loader: () => makeLargePageLevelData(5, 0x5eed_1005),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelFiveComponent,
+})
+
+function LevelFiveComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx
new file mode 100644
index 0000000000..71ec922804
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4')({
+ loader: () => makeLargePageLevelData(4, 0x5eed_1004),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelFourComponent,
+})
+
+function LevelFourComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx
new file mode 100644
index 0000000000..414a93dff8
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3')({
+ loader: () => makeLargePageLevelData(3, 0x5eed_1003),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelThreeComponent,
+})
+
+function LevelThreeComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx
new file mode 100644
index 0000000000..df2531641e
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2')({
+ loader: () => makeLargePageLevelData(2, 0x5eed_1002),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelTwoComponent,
+})
+
+function LevelTwoComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx
new file mode 100644
index 0000000000..0dbfc839c1
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1')({
+ loader: () => makeLargePageLevelData(1, 0x5eed_1001),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelOneComponent,
+})
+
+function LevelOneComponent() {
+ const data = Route.useLoaderData()
+ const first = () => data().records[0]!
+
+ return (
+
+ {data().marker}
+ records: {data().records.length}
+
+ {first().name}
+ {first().id}
+ {first().description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json
new file mode 100644
index 0000000000..4b61264e11
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts
new file mode 100644
index 0000000000..ffc80b43aa
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import solid from 'vite-plugin-solid'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ solid({ ssr: true, hot: false, dev: false }),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server peak-large-page (solid)',
+ watch: false,
+ environment: 'node',
+ server: {
+ deps: {
+ inline: [/@solidjs/, /@tanstack\/solid-store/],
+ },
+ },
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts
new file mode 100644
index 0000000000..1dc2e0da86
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts
@@ -0,0 +1,10 @@
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
+
+const test = await setup()
+await test.sanity()
+
+describe('memory', () => {
+ registerServerMemoryBenches(test)
+})
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json b/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json
new file mode 100644
index 0000000000..f424a2134f
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-server-peak-large-page-vue",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/vue-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts
new file mode 100644
index 0000000000..df2908aec9
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts
@@ -0,0 +1,14 @@
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ return createSetup('vue', handler)
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts
new file mode 100644
index 0000000000..2b59e96d72
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts
@@ -0,0 +1,111 @@
+export interface LargePageRecord {
+ id: string
+ name: string
+ description: string
+}
+
+export interface LargePageLevelData {
+ level: number
+ marker: string
+ records: Array
+}
+
+const recordCount = 200
+const descriptionLength = 104
+
+export function makeLargePageLevelData(level: number, seed: number) {
+ const random = createDeterministicRandom(seed)
+
+ return {
+ level,
+ marker: makeLargePageMarker(level),
+ records: Array.from({ length: recordCount }, (_, index) => {
+ const idToken = randomSegment(random)
+ const descriptionTokenA = randomSegment(random)
+ const descriptionTokenB = randomSegment(random)
+
+ return {
+ id: `l${level}-${index}-${idToken}`,
+ name: makeLargePageRecordName(level, index),
+ description: makeDescription(
+ level,
+ index,
+ descriptionTokenA,
+ descriptionTokenB,
+ ),
+ }
+ }),
+ } satisfies LargePageLevelData
+}
+
+export function makeLargePageHead(loaderData: LargePageLevelData | undefined) {
+ if (!loaderData) {
+ return {
+ meta: [{ title: 'Peak Large Page' }],
+ }
+ }
+
+ const first = loaderData.records[0]!
+ const last = loaderData.records[loaderData.records.length - 1]!
+
+ return {
+ meta: [
+ { title: `Peak Large Page L${loaderData.level} ${first.name}` },
+ {
+ name: `peak-large-page-l${loaderData.level}-count`,
+ content: String(loaderData.records.length),
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-first-id`,
+ content: first.id,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-first-name`,
+ content: first.name,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-last-id`,
+ content: last.id,
+ },
+ {
+ name: `peak-large-page-l${loaderData.level}-description`,
+ content: first.description,
+ },
+ ],
+ }
+}
+
+function makeLargePageRecordName(level: number, index: number) {
+ return `peak-large-page-l${level}-record-${index}`
+}
+
+function makeLargePageMarker(level: number) {
+ return `peak-large-page-level-${level}`
+}
+
+function makeDescription(
+ level: number,
+ index: number,
+ tokenA: string,
+ tokenB: string,
+) {
+ return `Level ${level} record ${index} uses seeded fragments ${tokenA} and ${tokenB} to keep a fresh deterministic loader payload.`.padEnd(
+ descriptionLength,
+ 'x',
+ )
+}
+
+// Local copy of the LCG from benchmarks/memory/server/bench-utils.ts; app
+// source cannot import from outside the app root. Keep in sync.
+function createDeterministicRandom(seed: number) {
+ let state = seed >>> 0
+
+ return () => {
+ state = (state * 1664525 + 1013904223) >>> 0
+ return state / 0x100000000
+ }
+}
+
+function randomSegment(random: () => number) {
+ return Math.floor(random() * 1_000_000_000).toString(36)
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts
new file mode 100644
index 0000000000..26cc9f6058
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts
@@ -0,0 +1,305 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as L1RouteImport } from './routes/l1'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as L1L2RouteImport } from './routes/l1.l2'
+import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3'
+import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4'
+import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5'
+import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6'
+import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7'
+import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8'
+
+const L1Route = L1RouteImport.update({
+ id: '/l1',
+ path: '/l1',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const L1L2Route = L1L2RouteImport.update({
+ id: '/l2',
+ path: '/l2',
+ getParentRoute: () => L1Route,
+} as any)
+const L1L2L3Route = L1L2L3RouteImport.update({
+ id: '/l3',
+ path: '/l3',
+ getParentRoute: () => L1L2Route,
+} as any)
+const L1L2L3L4Route = L1L2L3L4RouteImport.update({
+ id: '/l4',
+ path: '/l4',
+ getParentRoute: () => L1L2L3Route,
+} as any)
+const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({
+ id: '/l5',
+ path: '/l5',
+ getParentRoute: () => L1L2L3L4Route,
+} as any)
+const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({
+ id: '/l6',
+ path: '/l6',
+ getParentRoute: () => L1L2L3L4L5Route,
+} as any)
+const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({
+ id: '/l7',
+ path: '/l7',
+ getParentRoute: () => L1L2L3L4L5L6Route,
+} as any)
+const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({
+ id: '/l8',
+ path: '/l8',
+ getParentRoute: () => L1L2L3L4L5L6L7Route,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/l1': typeof L1RouteWithChildren
+ '/l1/l2': typeof L1L2RouteWithChildren
+ '/l1/l2/l3': typeof L1L2L3RouteWithChildren
+ '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren
+ '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren
+ '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ id:
+ | '__root__'
+ | '/'
+ | '/l1'
+ | '/l1/l2'
+ | '/l1/l2/l3'
+ | '/l1/l2/l3/l4'
+ | '/l1/l2/l3/l4/l5'
+ | '/l1/l2/l3/l4/l5/l6'
+ | '/l1/l2/l3/l4/l5/l6/l7'
+ | '/l1/l2/l3/l4/l5/l6/l7/l8'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ L1Route: typeof L1RouteWithChildren
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/l1': {
+ id: '/l1'
+ path: '/l1'
+ fullPath: '/l1'
+ preLoaderRoute: typeof L1RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/l1/l2': {
+ id: '/l1/l2'
+ path: '/l2'
+ fullPath: '/l1/l2'
+ preLoaderRoute: typeof L1L2RouteImport
+ parentRoute: typeof L1Route
+ }
+ '/l1/l2/l3': {
+ id: '/l1/l2/l3'
+ path: '/l3'
+ fullPath: '/l1/l2/l3'
+ preLoaderRoute: typeof L1L2L3RouteImport
+ parentRoute: typeof L1L2Route
+ }
+ '/l1/l2/l3/l4': {
+ id: '/l1/l2/l3/l4'
+ path: '/l4'
+ fullPath: '/l1/l2/l3/l4'
+ preLoaderRoute: typeof L1L2L3L4RouteImport
+ parentRoute: typeof L1L2L3Route
+ }
+ '/l1/l2/l3/l4/l5': {
+ id: '/l1/l2/l3/l4/l5'
+ path: '/l5'
+ fullPath: '/l1/l2/l3/l4/l5'
+ preLoaderRoute: typeof L1L2L3L4L5RouteImport
+ parentRoute: typeof L1L2L3L4Route
+ }
+ '/l1/l2/l3/l4/l5/l6': {
+ id: '/l1/l2/l3/l4/l5/l6'
+ path: '/l6'
+ fullPath: '/l1/l2/l3/l4/l5/l6'
+ preLoaderRoute: typeof L1L2L3L4L5L6RouteImport
+ parentRoute: typeof L1L2L3L4L5Route
+ }
+ '/l1/l2/l3/l4/l5/l6/l7': {
+ id: '/l1/l2/l3/l4/l5/l6/l7'
+ path: '/l7'
+ fullPath: '/l1/l2/l3/l4/l5/l6/l7'
+ preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport
+ parentRoute: typeof L1L2L3L4L5L6Route
+ }
+ '/l1/l2/l3/l4/l5/l6/l7/l8': {
+ id: '/l1/l2/l3/l4/l5/l6/l7/l8'
+ path: '/l8'
+ fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8'
+ preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport
+ parentRoute: typeof L1L2L3L4L5L6L7Route
+ }
+ }
+}
+
+interface L1L2L3L4L5L6L7RouteChildren {
+ L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route
+}
+
+const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = {
+ L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route,
+}
+
+const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren(
+ L1L2L3L4L5L6L7RouteChildren,
+)
+
+interface L1L2L3L4L5L6RouteChildren {
+ L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren
+}
+
+const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = {
+ L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren,
+}
+
+const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren(
+ L1L2L3L4L5L6RouteChildren,
+)
+
+interface L1L2L3L4L5RouteChildren {
+ L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren
+}
+
+const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = {
+ L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren,
+}
+
+const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren(
+ L1L2L3L4L5RouteChildren,
+)
+
+interface L1L2L3L4RouteChildren {
+ L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren
+}
+
+const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = {
+ L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren,
+}
+
+const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren(
+ L1L2L3L4RouteChildren,
+)
+
+interface L1L2L3RouteChildren {
+ L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren
+}
+
+const L1L2L3RouteChildren: L1L2L3RouteChildren = {
+ L1L2L3L4Route: L1L2L3L4RouteWithChildren,
+}
+
+const L1L2L3RouteWithChildren =
+ L1L2L3Route._addFileChildren(L1L2L3RouteChildren)
+
+interface L1L2RouteChildren {
+ L1L2L3Route: typeof L1L2L3RouteWithChildren
+}
+
+const L1L2RouteChildren: L1L2RouteChildren = {
+ L1L2L3Route: L1L2L3RouteWithChildren,
+}
+
+const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren)
+
+interface L1RouteChildren {
+ L1L2Route: typeof L1L2RouteWithChildren
+}
+
+const L1RouteChildren: L1RouteChildren = {
+ L1L2Route: L1L2RouteWithChildren,
+}
+
+const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ L1Route: L1RouteWithChildren,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/vue-start'
+declare module '@tanstack/vue-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx
new file mode 100644
index 0000000000..4290e7cdd3
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx
new file mode 100644
index 0000000000..de29ee1612
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx
@@ -0,0 +1,29 @@
+import {
+ Body,
+ HeadContent,
+ Html,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charSet: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx
new file mode 100644
index 0000000000..e9a57d46fd
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return peak-large-page-index
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
new file mode 100644
index 0000000000..61b52162f1
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({
+ loader: () => makeLargePageLevelData(8, 0x5eed_1008),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelEightComponent,
+})
+
+function LevelEightComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
new file mode 100644
index 0000000000..efbcadff76
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({
+ loader: () => makeLargePageLevelData(7, 0x5eed_1007),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelSevenComponent,
+})
+
+function LevelSevenComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx
new file mode 100644
index 0000000000..ff1550045e
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({
+ loader: () => makeLargePageLevelData(6, 0x5eed_1006),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelSixComponent,
+})
+
+function LevelSixComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx
new file mode 100644
index 0000000000..c6894302ed
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4/l5')({
+ loader: () => makeLargePageLevelData(5, 0x5eed_1005),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelFiveComponent,
+})
+
+function LevelFiveComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx
new file mode 100644
index 0000000000..99a4c880f9
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3/l4')({
+ loader: () => makeLargePageLevelData(4, 0x5eed_1004),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelFourComponent,
+})
+
+function LevelFourComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx
new file mode 100644
index 0000000000..8e6ac1e36c
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2/l3')({
+ loader: () => makeLargePageLevelData(3, 0x5eed_1003),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelThreeComponent,
+})
+
+function LevelThreeComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx
new file mode 100644
index 0000000000..074b3465eb
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1/l2')({
+ loader: () => makeLargePageLevelData(2, 0x5eed_1002),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelTwoComponent,
+})
+
+function LevelTwoComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx
new file mode 100644
index 0000000000..f11410cdbe
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data'
+
+export const Route = createFileRoute('/l1')({
+ loader: () => makeLargePageLevelData(1, 0x5eed_1001),
+ head: ({ loaderData }) => makeLargePageHead(loaderData),
+ component: LevelOneComponent,
+})
+
+function LevelOneComponent() {
+ const data = Route.useLoaderData()
+ const first = data.value.records[0]!
+
+ return (
+
+ {data.value.marker}
+ records: {data.value.records.length}
+
+ {first.name}
+ {first.id}
+ {first.description}
+
+
+
+ )
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json
new file mode 100644
index 0000000000..9ad6481342
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../../tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "types": ["node", "vite/client", "vitest/globals"]
+ },
+ "include": [
+ "memory.bench.ts",
+ "memory.flame.ts",
+ "setup.ts",
+ "vite.config.ts",
+ "../../../bench-utils.ts",
+ "./src/**/*"
+ ]
+}
diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts
new file mode 100644
index 0000000000..67bdf9b79d
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts
@@ -0,0 +1,29 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+import codspeedPlugin from '@codspeed/vitest-plugin'
+import { tanstackStart } from '@tanstack/vue-start/plugin/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+const rootDir = fileURLToPath(new URL('.', import.meta.url))
+
+export default defineConfig({
+ root: rootDir,
+ plugins: [
+ !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) &&
+ codspeedPlugin(),
+ tanstackStart({
+ srcDirectory: 'src',
+ }),
+ vueJsx(),
+ ],
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ minify: false,
+ },
+ test: {
+ name: '@benchmarks/memory-server peak-large-page (vue)',
+ watch: false,
+ environment: 'node',
+ },
+})
diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
index 8da5590818..66aed269b9 100644
--- a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
+++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts
@@ -1,65 +1,7 @@
-import {
- createDeterministicRandom,
- randomSegment,
- runSequentialRequestLoop,
-} from '#memory-server/bench-utils'
-import type { StartRequestHandler } from '#memory-server/bench-utils'
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
-const benchmarkSeed = 0xdecafbad
-const requestChurnIterations = 200
-const itemPageMarker = 'data-bench="request-churn-item"'
-const dehydrationMarker = '$_TSR'
-// Module-level so CodSpeed warmups and measurement never replay URLs.
-const benchmarkRandom = createDeterministicRandom(benchmarkSeed)
-let requestCounter = 0
-
-const requestInit = {
- method: 'GET',
- headers: {
- accept: 'text/html',
- },
-} satisfies RequestInit
-
-function buildItemRequest(random: () => number) {
- const id = `${(requestCounter++).toString(36)}-${randomSegment(random)}`
- const q = `q-${randomSegment(random)}`
-
- return new Request(`http://localhost/items/${id}?q=${q}`, requestInit)
-}
-
-function validateItemResponse(response: Response, request: Request) {
- if (response.status !== 200) {
- throw new Error(
- `Expected status 200 for ${request.url}, got ${response.status}`,
- )
- }
-}
-
-function validateItemBody(body: string) {
- if (!body.includes(itemPageMarker)) {
- throw new Error('Expected request-churn item marker in response body')
- }
-}
-
-async function assertRequestChurnSanity(handler: StartRequestHandler) {
- const response = await handler.fetch(
- new Request('http://localhost/items/sanity-item?q=q-sanity', requestInit),
- )
- const body = await response.text()
-
- if (response.status !== 200) {
- throw new Error(`Expected sanity status 200, got ${response.status}`)
- }
-
- validateItemBody(body)
-
- if (!body.includes(dehydrationMarker)) {
- throw new Error(
- 'Expected sanity response to include the dehydration marker',
- )
- }
-}
export async function setup() {
const { default: handler } = (await import(
@@ -68,21 +10,5 @@ export async function setup() {
default: StartRequestHandler
}
- const run = () =>
- runSequentialRequestLoop(handler, {
- random: benchmarkRandom,
- iterations: requestChurnIterations,
- buildRequest: buildItemRequest,
- validateResponse: validateItemResponse,
- })
-
- return {
- sanity: () => assertRequestChurnSanity(handler),
- benches: [
- {
- name: 'mem request-churn (react)',
- run,
- },
- ],
- }
+ return createSetup('react', handler)
}
diff --git a/benchmarks/memory/server/scenarios/request-churn/shared.ts b/benchmarks/memory/server/scenarios/request-churn/shared.ts
new file mode 100644
index 0000000000..d29c3c9685
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/shared.ts
@@ -0,0 +1,89 @@
+import {
+ createDeterministicRandom,
+ randomSegment,
+ runSequentialRequestLoop,
+} from '#memory-server/bench-utils'
+import type { StartRequestHandler } from '#memory-server/bench-utils'
+
+export type { StartRequestHandler }
+
+type Framework = 'react' | 'solid' | 'vue'
+
+const benchmarkSeed = 0xdecafbad
+const requestChurnIterations = 200
+const itemPageMarker = 'data-bench="request-churn-item"'
+const dehydrationMarker = '$_TSR'
+// Module-level so CodSpeed warmups and measurement never replay URLs.
+const benchmarkRandom = createDeterministicRandom(benchmarkSeed)
+let requestCounter = 0
+
+const requestInit = {
+ method: 'GET',
+ headers: {
+ accept: 'text/html',
+ },
+} satisfies RequestInit
+
+function validateItemResponse(response: Response, request: Request) {
+ if (response.status !== 200) {
+ throw new Error(
+ `Expected status 200 for ${request.url}, got ${response.status}`,
+ )
+ }
+}
+
+function validateItemBody(body: string) {
+ if (!body.includes(itemPageMarker)) {
+ throw new Error('Expected request-churn item marker in response body')
+ }
+}
+
+async function assertRequestChurnSanity(handler: StartRequestHandler) {
+ const response = await handler.fetch(
+ new Request('http://localhost/items/sanity-item?q=q-sanity', requestInit),
+ )
+ const body = await response.text()
+
+ if (response.status !== 200) {
+ throw new Error(`Expected sanity status 200, got ${response.status}`)
+ }
+
+ validateItemBody(body)
+
+ if (!body.includes(dehydrationMarker)) {
+ throw new Error(
+ 'Expected sanity response to include the dehydration marker',
+ )
+ }
+}
+
+export function createSetup(
+ framework: Framework,
+ handler: StartRequestHandler,
+) {
+ function buildItemRequest(random: () => number) {
+ const counter = (requestCounter++).toString(36)
+ const id = `${counter}-${randomSegment(random)}`
+ const q = `q-${randomSegment(random)}`
+
+ return new Request(`http://localhost/items/${id}?q=${q}`, requestInit)
+ }
+
+ const run = () =>
+ runSequentialRequestLoop(handler, {
+ random: benchmarkRandom,
+ iterations: requestChurnIterations,
+ buildRequest: buildItemRequest,
+ validateResponse: validateItemResponse,
+ })
+
+ return {
+ sanity: () => assertRequestChurnSanity(handler),
+ benches: [
+ {
+ name: `mem request-churn (${framework})`,
+ run,
+ },
+ ],
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts
new file mode 100644
index 0000000000..1dc2e0da86
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts
@@ -0,0 +1,10 @@
+import { describe } from 'vitest'
+import { registerServerMemoryBenches } from '#memory-server/runner'
+import { setup } from './setup'
+
+const test = await setup()
+await test.sanity()
+
+describe('memory', () => {
+ registerServerMemoryBenches(test)
+})
diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts
new file mode 100644
index 0000000000..882050f597
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts
@@ -0,0 +1,4 @@
+import { runServerFlameBenchmark } from '#memory-server/flame-runner'
+import { setup } from './setup.ts'
+
+await runServerFlameBenchmark(setup)
diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/project.json b/benchmarks/memory/server/scenarios/request-churn/solid/project.json
new file mode 100644
index 0000000000..3f3434ee24
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/solid/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "@benchmarks/memory-server-request-churn-solid",
+ "projectType": "application",
+ "targets": {
+ "build:ssr": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
+ }
+ },
+ "build:ssr:flame": {
+ "executor": "nx:run-commands",
+ "cache": false,
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true"
+ }
+ },
+ "test:flame": {
+ "executor": "nx:run-commands",
+ "parallelism": false,
+ "cache": false,
+ "dependsOn": ["build:ssr:flame"],
+ "options": {
+ "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist",
+ "cwd": "."
+ }
+ },
+ "test:types:ssr": {
+ "executor": "nx:run-commands",
+ "dependsOn": [
+ {
+ "projects": ["@tanstack/solid-start"],
+ "target": "build"
+ }
+ ],
+ "options": {
+ "command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
+ }
+ }
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts
new file mode 100644
index 0000000000..0a575854b5
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts
@@ -0,0 +1,14 @@
+import { createSetup } from '../shared'
+import type { StartRequestHandler } from '../shared'
+
+const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href
+
+export async function setup() {
+ const { default: handler } = (await import(
+ /* @vite-ignore */ appModuleUrl
+ )) as {
+ default: StartRequestHandler
+ }
+
+ return createSetup('solid', handler)
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts
new file mode 100644
index 0000000000..8f49ec5d1f
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as ItemsIdRouteImport } from './routes/items.$id'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ItemsIdRoute = ItemsIdRouteImport.update({
+ id: '/items/$id',
+ path: '/items/$id',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/items/$id': typeof ItemsIdRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/items/$id'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/items/$id'
+ id: '__root__' | '/' | '/items/$id'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ ItemsIdRoute: typeof ItemsIdRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/items/$id': {
+ id: '/items/$id'
+ path: '/items/$id'
+ fullPath: '/items/$id'
+ preLoaderRoute: typeof ItemsIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ ItemsIdRoute: ItemsIdRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/solid-start'
+declare module '@tanstack/solid-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx
new file mode 100644
index 0000000000..038ec0ab5e
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ return createRouter({
+ routeTree,
+ defaultPreload: false,
+ scrollRestoration: false,
+ })
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx
new file mode 100644
index 0000000000..aaf7dfdd89
--- /dev/null
+++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx
@@ -0,0 +1,29 @@
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { HydrationScript } from 'solid-js/web'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [{ charset: 'utf-8' }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+