From 8a462404cff26d2b649d891cd342c9f04f67dd00 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 2 Jul 2026 16:23:36 +0200 Subject: [PATCH 1/7] feat(benchmarks): add client-side CPU benchmark scenario suite Add 11 client-side CPU benchmark scenarios to benchmarks/client-nav, each implemented identically across react/solid/vue and tracked by CodSpeed through the existing Benchmarks workflow (no CI changes needed): async-pipeline, control-flow, head, history, links, loaders, mount, nested-params, preload, route-tree-scale, search-params Each scenario is an isolated file-based-routing app (route trees generated by @tanstack/router-plugin) built for production and driven in jsdom by a shared harness (scenarios/harness.ts): circular step sequences of real clicks (plus hover/preload/history-traversal/invalidate steps), each synchronized on the router's onRendered event, with a warm-up lap that asserts every step's observable output. Determinism rules: no wall-clock timers (counted 0ms hops only), staleTime/gcTime pinned to 0 or 1e9, deterministic preload staleness, stationary DOM and history depth. The existing baseline apps and bench names are untouched for CodSpeed continuity; baseline vite configs only gain an explicit root/setupFiles so they resolve identically under the new per-framework aggregate configs. Notable constraints encoded in the scenarios: - Component-level Await/Suspense is excluded from async-pipeline: React 19 throttles every Suspense reveal by ~300ms wall-clock, which is inherently non-deterministic to benchmark. - control-flow/react silences React 19's onCaughtError reporting, which otherwise dominates the measured loop with console I/O. - Vue route components must be defined before their createFileRoute call (bundled var hoisting silently yields component: undefined otherwise). Co-Authored-By: Claude Fable 5 --- benchmarks/client-nav/README.md | 84 +- benchmarks/client-nav/jsdom.ts | 6 + benchmarks/client-nav/package.json | 95 +- benchmarks/client-nav/react/vite.config.ts | 8 +- .../async-pipeline/react/project.json | 40 + .../scenarios/async-pipeline/react/setup.ts | 17 + .../async-pipeline/react/speed.bench.ts | 34 + .../async-pipeline/react/speed.flame.ts | 18 + .../async-pipeline/react/src/main.tsx | 32 + .../async-pipeline/react/src/routeTree.gen.ts | 140 +++ .../react/src/routes/__root.tsx | 50 + .../react/src/routes/ctx.$id.tsx | 29 + .../async-pipeline/react/src/routes/index.tsx | 13 + .../react/src/routes/nested.$id.tsx | 18 + .../react/src/routes/nested.tsx | 23 + .../react/src/routes/slow.$id.tsx | 18 + .../async-pipeline/react/tsconfig.json | 21 + .../async-pipeline/react/vite.config.ts | 40 + .../scenarios/async-pipeline/shared.ts | 107 +++ .../async-pipeline/solid/project.json | 40 + .../scenarios/async-pipeline/solid/setup.ts | 17 + .../async-pipeline/solid/speed.bench.ts | 34 + .../async-pipeline/solid/speed.flame.ts | 18 + .../async-pipeline/solid/src/main.tsx | 29 + .../async-pipeline/solid/src/routeTree.gen.ts | 140 +++ .../solid/src/routes/__root.tsx | 46 + .../solid/src/routes/ctx.$id.tsx | 29 + .../async-pipeline/solid/src/routes/index.tsx | 13 + .../solid/src/routes/nested.$id.tsx | 18 + .../solid/src/routes/nested.tsx | 23 + .../solid/src/routes/slow.$id.tsx | 18 + .../async-pipeline/solid/tsconfig.json | 21 + .../async-pipeline/solid/vite.config.ts | 48 + .../scenarios/async-pipeline/vue/project.json | 40 + .../scenarios/async-pipeline/vue/setup.ts | 17 + .../async-pipeline/vue/speed.bench.ts | 34 + .../async-pipeline/vue/speed.flame.ts | 18 + .../scenarios/async-pipeline/vue/src/main.tsx | 36 + .../async-pipeline/vue/src/routeTree.gen.ts | 140 +++ .../async-pipeline/vue/src/routes/__root.tsx | 53 ++ .../async-pipeline/vue/src/routes/ctx.$id.tsx | 32 + .../async-pipeline/vue/src/routes/index.tsx | 16 + .../vue/src/routes/nested.$id.tsx | 21 + .../async-pipeline/vue/src/routes/nested.tsx | 26 + .../vue/src/routes/slow.$id.tsx | 21 + .../async-pipeline/vue/tsconfig.json | 21 + .../async-pipeline/vue/vite.config.ts | 42 + .../scenarios/control-flow/react/project.json | 40 + .../scenarios/control-flow/react/setup.ts | 17 + .../control-flow/react/speed.bench.ts | 34 + .../control-flow/react/speed.flame.ts | 18 + .../scenarios/control-flow/react/src/main.tsx | 32 + .../control-flow/react/src/routeTree.gen.ts | 156 ++++ .../control-flow/react/src/routes/__root.tsx | 52 ++ .../control-flow/react/src/routes/broken.tsx | 16 + .../control-flow/react/src/routes/hop1.tsx | 9 + .../control-flow/react/src/routes/hop2.tsx | 9 + .../control-flow/react/src/routes/index.tsx | 9 + .../react/src/routes/missing.$id.tsx | 23 + .../control-flow/react/src/routes/target.tsx | 9 + .../control-flow/react/tsconfig.json | 21 + .../control-flow/react/vite.config.ts | 40 + .../scenarios/control-flow/shared.ts | 91 ++ .../scenarios/control-flow/solid/project.json | 40 + .../scenarios/control-flow/solid/setup.ts | 17 + .../control-flow/solid/speed.bench.ts | 34 + .../control-flow/solid/speed.flame.ts | 18 + .../scenarios/control-flow/solid/src/main.tsx | 27 + .../control-flow/solid/src/routeTree.gen.ts | 156 ++++ .../control-flow/solid/src/routes/__root.tsx | 52 ++ .../control-flow/solid/src/routes/broken.tsx | 16 + .../control-flow/solid/src/routes/hop1.tsx | 9 + .../control-flow/solid/src/routes/hop2.tsx | 9 + .../control-flow/solid/src/routes/index.tsx | 9 + .../solid/src/routes/missing.$id.tsx | 23 + .../control-flow/solid/src/routes/target.tsx | 9 + .../control-flow/solid/tsconfig.json | 21 + .../control-flow/solid/vite.config.ts | 48 + .../scenarios/control-flow/vue/project.json | 40 + .../scenarios/control-flow/vue/setup.ts | 17 + .../scenarios/control-flow/vue/speed.bench.ts | 34 + .../scenarios/control-flow/vue/speed.flame.ts | 18 + .../scenarios/control-flow/vue/src/main.tsx | 34 + .../control-flow/vue/src/routeTree.gen.ts | 156 ++++ .../control-flow/vue/src/routes/__root.tsx | 57 ++ .../control-flow/vue/src/routes/broken.tsx | 21 + .../control-flow/vue/src/routes/hop1.tsx | 9 + .../control-flow/vue/src/routes/hop2.tsx | 9 + .../control-flow/vue/src/routes/index.tsx | 12 + .../vue/src/routes/missing.$id.tsx | 28 + .../control-flow/vue/src/routes/target.tsx | 12 + .../scenarios/control-flow/vue/tsconfig.json | 21 + .../scenarios/control-flow/vue/vite.config.ts | 42 + benchmarks/client-nav/scenarios/harness.ts | 298 ++++++ .../scenarios/head/react/project.json | 40 + .../client-nav/scenarios/head/react/setup.ts | 17 + .../scenarios/head/react/speed.bench.ts | 34 + .../scenarios/head/react/speed.flame.ts | 18 + .../scenarios/head/react/src/main.tsx | 30 + .../scenarios/head/react/src/routeTree.gen.ts | 145 +++ .../head/react/src/routes/__root.tsx | 59 ++ .../head/react/src/routes/articles.$id.tsx | 18 + .../head/react/src/routes/docs.$section.tsx | 18 + .../scenarios/head/react/src/routes/docs.tsx | 16 + .../scenarios/head/react/src/routes/index.tsx | 16 + .../head/react/src/routes/settings.tsx | 16 + .../scenarios/head/react/tsconfig.json | 21 + .../scenarios/head/react/vite.config.ts | 40 + .../client-nav/scenarios/head/shared.ts | 152 +++ .../scenarios/head/solid/project.json | 40 + .../client-nav/scenarios/head/solid/setup.ts | 17 + .../scenarios/head/solid/speed.bench.ts | 34 + .../scenarios/head/solid/speed.flame.ts | 18 + .../scenarios/head/solid/src/main.tsx | 27 + .../scenarios/head/solid/src/routeTree.gen.ts | 145 +++ .../head/solid/src/routes/__root.tsx | 58 ++ .../head/solid/src/routes/articles.$id.tsx | 18 + .../head/solid/src/routes/docs.$section.tsx | 18 + .../scenarios/head/solid/src/routes/docs.tsx | 16 + .../scenarios/head/solid/src/routes/index.tsx | 16 + .../head/solid/src/routes/settings.tsx | 16 + .../scenarios/head/solid/tsconfig.json | 21 + .../scenarios/head/solid/vite.config.ts | 48 + .../scenarios/head/vue/project.json | 40 + .../client-nav/scenarios/head/vue/setup.ts | 17 + .../scenarios/head/vue/speed.bench.ts | 34 + .../scenarios/head/vue/speed.flame.ts | 18 + .../scenarios/head/vue/src/main.tsx | 34 + .../scenarios/head/vue/src/routeTree.gen.ts | 145 +++ .../scenarios/head/vue/src/routes/__root.tsx | 58 ++ .../head/vue/src/routes/articles.$id.tsx | 21 + .../head/vue/src/routes/docs.$section.tsx | 21 + .../scenarios/head/vue/src/routes/docs.tsx | 19 + .../scenarios/head/vue/src/routes/index.tsx | 19 + .../head/vue/src/routes/settings.tsx | 19 + .../scenarios/head/vue/tsconfig.json | 21 + .../scenarios/head/vue/vite.config.ts | 42 + .../scenarios/history/react/project.json | 40 + .../scenarios/history/react/setup.ts | 17 + .../scenarios/history/react/speed.bench.ts | 34 + .../scenarios/history/react/speed.flame.ts | 18 + .../scenarios/history/react/src/main.tsx | 30 + .../history/react/src/routeTree.gen.ts | 113 +++ .../history/react/src/routes/__root.tsx | 66 ++ .../history/react/src/routes/gallery.tsx | 13 + .../history/react/src/routes/index.tsx | 13 + .../history/react/src/routes/pages.$n.tsx | 16 + .../react/src/routes/photos.$photoId.tsx | 16 + .../scenarios/history/react/tsconfig.json | 21 + .../scenarios/history/react/vite.config.ts | 40 + .../client-nav/scenarios/history/shared.ts | 89 ++ .../scenarios/history/solid/project.json | 40 + .../scenarios/history/solid/setup.ts | 17 + .../scenarios/history/solid/speed.bench.ts | 34 + .../scenarios/history/solid/speed.flame.ts | 18 + .../scenarios/history/solid/src/main.tsx | 27 + .../history/solid/src/routeTree.gen.ts | 113 +++ .../history/solid/src/routes/__root.tsx | 66 ++ .../history/solid/src/routes/gallery.tsx | 13 + .../history/solid/src/routes/index.tsx | 13 + .../history/solid/src/routes/pages.$n.tsx | 16 + .../solid/src/routes/photos.$photoId.tsx | 16 + .../scenarios/history/solid/tsconfig.json | 21 + .../scenarios/history/solid/vite.config.ts | 48 + .../scenarios/history/vue/project.json | 40 + .../client-nav/scenarios/history/vue/setup.ts | 17 + .../scenarios/history/vue/speed.bench.ts | 34 + .../scenarios/history/vue/speed.flame.ts | 18 + .../scenarios/history/vue/src/main.tsx | 34 + .../history/vue/src/routeTree.gen.ts | 113 +++ .../history/vue/src/routes/__root.tsx | 69 ++ .../history/vue/src/routes/gallery.tsx | 16 + .../history/vue/src/routes/index.tsx | 16 + .../history/vue/src/routes/pages.$n.tsx | 19 + .../vue/src/routes/photos.$photoId.tsx | 19 + .../scenarios/history/vue/tsconfig.json | 21 + .../scenarios/history/vue/vite.config.ts | 42 + .../scenarios/links/react/project.json | 40 + .../client-nav/scenarios/links/react/setup.ts | 17 + .../scenarios/links/react/speed.bench.ts | 34 + .../scenarios/links/react/speed.flame.ts | 18 + .../scenarios/links/react/src/main.tsx | 30 + .../links/react/src/routeTree.gen.ts | 95 ++ .../links/react/src/routes/__root.tsx | 103 ++ .../links/react/src/routes/about.tsx | 14 + .../links/react/src/routes/index.tsx | 14 + .../links/react/src/routes/items.$id.tsx | 16 + .../scenarios/links/react/tsconfig.json | 21 + .../scenarios/links/react/vite.config.ts | 40 + .../client-nav/scenarios/links/shared.ts | 133 +++ .../scenarios/links/solid/project.json | 40 + .../client-nav/scenarios/links/solid/setup.ts | 17 + .../scenarios/links/solid/speed.bench.ts | 34 + .../scenarios/links/solid/speed.flame.ts | 18 + .../scenarios/links/solid/src/main.tsx | 27 + .../links/solid/src/routeTree.gen.ts | 95 ++ .../links/solid/src/routes/__root.tsx | 118 +++ .../links/solid/src/routes/about.tsx | 14 + .../links/solid/src/routes/index.tsx | 14 + .../links/solid/src/routes/items.$id.tsx | 16 + .../scenarios/links/solid/tsconfig.json | 21 + .../scenarios/links/solid/vite.config.ts | 48 + .../scenarios/links/vue/project.json | 40 + .../client-nav/scenarios/links/vue/setup.ts | 17 + .../scenarios/links/vue/speed.bench.ts | 34 + .../scenarios/links/vue/speed.flame.ts | 18 + .../scenarios/links/vue/src/main.tsx | 34 + .../scenarios/links/vue/src/routeTree.gen.ts | 95 ++ .../scenarios/links/vue/src/routes/__root.tsx | 122 +++ .../scenarios/links/vue/src/routes/about.tsx | 17 + .../scenarios/links/vue/src/routes/index.tsx | 17 + .../links/vue/src/routes/items.$id.tsx | 19 + .../scenarios/links/vue/tsconfig.json | 21 + .../scenarios/links/vue/vite.config.ts | 42 + .../scenarios/loaders/react/project.json | 40 + .../scenarios/loaders/react/setup.ts | 17 + .../scenarios/loaders/react/speed.bench.ts | 34 + .../scenarios/loaders/react/speed.flame.ts | 18 + .../scenarios/loaders/react/src/main.tsx | 30 + .../loaders/react/src/routeTree.gen.ts | 113 +++ .../loaders/react/src/routes/__root.tsx | 70 ++ .../loaders/react/src/routes/cached.$id.tsx | 55 ++ .../loaders/react/src/routes/deps.tsx | 65 ++ .../loaders/react/src/routes/fresh.$id.tsx | 55 ++ .../loaders/react/src/routes/index.tsx | 14 + .../scenarios/loaders/react/tsconfig.json | 21 + .../scenarios/loaders/react/vite.config.ts | 40 + .../client-nav/scenarios/loaders/shared.ts | 134 +++ .../scenarios/loaders/solid/project.json | 40 + .../scenarios/loaders/solid/setup.ts | 17 + .../scenarios/loaders/solid/speed.bench.ts | 34 + .../scenarios/loaders/solid/speed.flame.ts | 18 + .../scenarios/loaders/solid/src/main.tsx | 27 + .../loaders/solid/src/routeTree.gen.ts | 113 +++ .../loaders/solid/src/routes/__root.tsx | 66 ++ .../loaders/solid/src/routes/cached.$id.tsx | 62 ++ .../loaders/solid/src/routes/deps.tsx | 72 ++ .../loaders/solid/src/routes/fresh.$id.tsx | 62 ++ .../loaders/solid/src/routes/index.tsx | 14 + .../scenarios/loaders/solid/tsconfig.json | 21 + .../scenarios/loaders/solid/vite.config.ts | 48 + .../scenarios/loaders/vue/project.json | 40 + .../client-nav/scenarios/loaders/vue/setup.ts | 17 + .../scenarios/loaders/vue/speed.bench.ts | 34 + .../scenarios/loaders/vue/speed.flame.ts | 18 + .../scenarios/loaders/vue/src/main.tsx | 34 + .../loaders/vue/src/routeTree.gen.ts | 113 +++ .../loaders/vue/src/routes/__root.tsx | 69 ++ .../loaders/vue/src/routes/cached.$id.tsx | 68 ++ .../scenarios/loaders/vue/src/routes/deps.tsx | 78 ++ .../loaders/vue/src/routes/fresh.$id.tsx | 68 ++ .../loaders/vue/src/routes/index.tsx | 17 + .../scenarios/loaders/vue/tsconfig.json | 21 + .../scenarios/loaders/vue/vite.config.ts | 42 + .../scenarios/mount/react/project.json | 40 + .../client-nav/scenarios/mount/react/setup.ts | 17 + .../scenarios/mount/react/speed.bench.ts | 34 + .../scenarios/mount/react/speed.flame.ts | 18 + .../scenarios/mount/react/src/main.tsx | 30 + .../mount/react/src/routeTree.gen.ts | 264 ++++++ .../mount/react/src/routes/__root.tsx | 41 + .../mount/react/src/routes/about.tsx | 9 + .../mount/react/src/routes/blog.$slug.tsx | 13 + .../mount/react/src/routes/blog.index.tsx | 9 + .../scenarios/mount/react/src/routes/blog.tsx | 16 + .../mount/react/src/routes/index.tsx | 22 + .../mount/react/src/routes/search.tsx | 16 + .../mount/react/src/routes/settings.tsx | 9 + .../react/src/routes/shop.$productId.tsx | 13 + .../mount/react/src/routes/shop.index.tsx | 9 + .../scenarios/mount/react/src/routes/shop.tsx | 16 + .../scenarios/mount/react/tsconfig.json | 21 + .../scenarios/mount/react/vite.config.ts | 40 + .../client-nav/scenarios/mount/shared.ts | 72 ++ .../scenarios/mount/solid/project.json | 40 + .../client-nav/scenarios/mount/solid/setup.ts | 17 + .../scenarios/mount/solid/speed.bench.ts | 34 + .../scenarios/mount/solid/speed.flame.ts | 18 + .../scenarios/mount/solid/src/main.tsx | 27 + .../mount/solid/src/routeTree.gen.ts | 264 ++++++ .../mount/solid/src/routes/__root.tsx | 41 + .../mount/solid/src/routes/about.tsx | 9 + .../mount/solid/src/routes/blog.$slug.tsx | 13 + .../mount/solid/src/routes/blog.index.tsx | 9 + .../scenarios/mount/solid/src/routes/blog.tsx | 16 + .../mount/solid/src/routes/index.tsx | 23 + .../mount/solid/src/routes/search.tsx | 16 + .../mount/solid/src/routes/settings.tsx | 9 + .../solid/src/routes/shop.$productId.tsx | 13 + .../mount/solid/src/routes/shop.index.tsx | 9 + .../scenarios/mount/solid/src/routes/shop.tsx | 16 + .../scenarios/mount/solid/tsconfig.json | 21 + .../scenarios/mount/solid/vite.config.ts | 48 + .../scenarios/mount/vue/project.json | 40 + .../client-nav/scenarios/mount/vue/setup.ts | 17 + .../scenarios/mount/vue/speed.bench.ts | 34 + .../scenarios/mount/vue/speed.flame.ts | 18 + .../scenarios/mount/vue/src/main.tsx | 34 + .../scenarios/mount/vue/src/routeTree.gen.ts | 264 ++++++ .../scenarios/mount/vue/src/routes/__root.tsx | 44 + .../scenarios/mount/vue/src/routes/about.tsx | 12 + .../mount/vue/src/routes/blog.$slug.tsx | 18 + .../mount/vue/src/routes/blog.index.tsx | 12 + .../scenarios/mount/vue/src/routes/blog.tsx | 19 + .../scenarios/mount/vue/src/routes/index.tsx | 27 + .../scenarios/mount/vue/src/routes/search.tsx | 21 + .../mount/vue/src/routes/settings.tsx | 12 + .../mount/vue/src/routes/shop.$productId.tsx | 18 + .../mount/vue/src/routes/shop.index.tsx | 12 + .../scenarios/mount/vue/src/routes/shop.tsx | 19 + .../scenarios/mount/vue/tsconfig.json | 21 + .../scenarios/mount/vue/vite.config.ts | 42 + .../nested-params/react/project.json | 40 + .../scenarios/nested-params/react/setup.ts | 17 + .../nested-params/react/speed.bench.ts | 34 + .../nested-params/react/speed.flame.ts | 18 + .../nested-params/react/src/main.tsx | 30 + .../nested-params/react/src/routeTree.gen.ts | 291 ++++++ .../nested-params/react/src/routes/__root.tsx | 50 + .../nested-params/react/src/routes/index.tsx | 9 + .../src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx | 63 ++ .../src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx | 51 + .../react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx | 51 + .../react/src/routes/l.$a.$b.$c.$d.$e.tsx | 51 + .../react/src/routes/l.$a.$b.$c.$d.tsx | 55 ++ .../react/src/routes/l.$a.$b.$c.tsx | 51 + .../react/src/routes/l.$a.$b.tsx | 51 + .../nested-params/react/src/routes/l.$a.tsx | 51 + .../nested-params/react/tsconfig.json | 21 + .../nested-params/react/vite.config.ts | 40 + .../scenarios/nested-params/shared.ts | 140 +++ .../nested-params/solid/project.json | 40 + .../scenarios/nested-params/solid/setup.ts | 17 + .../nested-params/solid/speed.bench.ts | 34 + .../nested-params/solid/speed.flame.ts | 18 + .../nested-params/solid/src/main.tsx | 27 + .../nested-params/solid/src/routeTree.gen.ts | 291 ++++++ .../nested-params/solid/src/routes/__root.tsx | 50 + .../nested-params/solid/src/routes/index.tsx | 9 + .../src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx | 72 ++ .../src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx | 60 ++ .../solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx | 60 ++ .../solid/src/routes/l.$a.$b.$c.$d.$e.tsx | 60 ++ .../solid/src/routes/l.$a.$b.$c.$d.tsx | 64 ++ .../solid/src/routes/l.$a.$b.$c.tsx | 60 ++ .../solid/src/routes/l.$a.$b.tsx | 60 ++ .../nested-params/solid/src/routes/l.$a.tsx | 60 ++ .../nested-params/solid/tsconfig.json | 21 + .../nested-params/solid/vite.config.ts | 48 + .../scenarios/nested-params/vue/project.json | 40 + .../scenarios/nested-params/vue/setup.ts | 17 + .../nested-params/vue/speed.bench.ts | 34 + .../nested-params/vue/speed.flame.ts | 18 + .../scenarios/nested-params/vue/src/main.tsx | 34 + .../nested-params/vue/src/routeTree.gen.ts | 291 ++++++ .../nested-params/vue/src/routes/__root.tsx | 53 ++ .../nested-params/vue/src/routes/index.tsx | 12 + .../src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx | 86 ++ .../vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx | 74 ++ .../vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx | 74 ++ .../vue/src/routes/l.$a.$b.$c.$d.$e.tsx | 74 ++ .../vue/src/routes/l.$a.$b.$c.$d.tsx | 83 ++ .../vue/src/routes/l.$a.$b.$c.tsx | 74 ++ .../nested-params/vue/src/routes/l.$a.$b.tsx | 74 ++ .../nested-params/vue/src/routes/l.$a.tsx | 74 ++ .../scenarios/nested-params/vue/tsconfig.json | 21 + .../nested-params/vue/vite.config.ts | 42 + .../scenarios/preload/react/project.json | 40 + .../scenarios/preload/react/setup.ts | 17 + .../scenarios/preload/react/speed.bench.ts | 34 + .../scenarios/preload/react/speed.flame.ts | 18 + .../scenarios/preload/react/src/main.tsx | 33 + .../preload/react/src/routeTree.gen.ts | 95 ++ .../preload/react/src/routes/__root.tsx | 32 + .../preload/react/src/routes/docs.tsx | 14 + .../preload/react/src/routes/index.tsx | 14 + .../react/src/routes/sections.$section.tsx | 32 + .../scenarios/preload/react/tsconfig.json | 21 + .../scenarios/preload/react/vite.config.ts | 40 + .../client-nav/scenarios/preload/shared.ts | 117 +++ .../scenarios/preload/solid/project.json | 40 + .../scenarios/preload/solid/setup.ts | 17 + .../scenarios/preload/solid/speed.bench.ts | 34 + .../scenarios/preload/solid/speed.flame.ts | 18 + .../scenarios/preload/solid/src/main.tsx | 30 + .../preload/solid/src/routeTree.gen.ts | 95 ++ .../preload/solid/src/routes/__root.tsx | 34 + .../preload/solid/src/routes/docs.tsx | 14 + .../preload/solid/src/routes/index.tsx | 14 + .../solid/src/routes/sections.$section.tsx | 33 + .../scenarios/preload/solid/tsconfig.json | 21 + .../scenarios/preload/solid/vite.config.ts | 48 + .../scenarios/preload/vue/project.json | 40 + .../client-nav/scenarios/preload/vue/setup.ts | 17 + .../scenarios/preload/vue/speed.bench.ts | 34 + .../scenarios/preload/vue/speed.flame.ts | 18 + .../scenarios/preload/vue/src/main.tsx | 37 + .../preload/vue/src/routeTree.gen.ts | 95 ++ .../preload/vue/src/routes/__root.tsx | 35 + .../scenarios/preload/vue/src/routes/docs.tsx | 17 + .../preload/vue/src/routes/index.tsx | 17 + .../vue/src/routes/sections.$section.tsx | 37 + .../scenarios/preload/vue/tsconfig.json | 21 + .../scenarios/preload/vue/vite.config.ts | 42 + .../route-tree-scale/react/project.json | 40 + .../scenarios/route-tree-scale/react/setup.ts | 17 + .../route-tree-scale/react/speed.bench.ts | 34 + .../route-tree-scale/react/speed.flame.ts | 18 + .../route-tree-scale/react/src/main.tsx | 30 + .../react/src/routeTree.gen.ts | 881 ++++++++++++++++++ .../react/src/routes/(marketing)/pricing.tsx | 9 + .../react/src/routes/(marketing)/promo.tsx | 9 + .../react/src/routes/__root.tsx | 46 + .../react/src/routes/_layout.alpha.tsx | 9 + .../react/src/routes/_layout.beta.tsx | 9 + .../react/src/routes/_layout.tsx | 9 + .../react/src/routes/files.$.tsx | 11 + .../react/src/routes/index.tsx | 9 + .../react/src/routes/release.v{$version}.tsx | 11 + .../react/src/routes/sec-a.$id.tsx | 11 + .../react/src/routes/sec-a.about.tsx | 9 + .../react/src/routes/sec-a.index.tsx | 9 + .../react/src/routes/sec-a.settings.tsx | 9 + .../react/src/routes/sec-a.tsx | 9 + .../react/src/routes/sec-b.$id.tsx | 11 + .../react/src/routes/sec-b.about.tsx | 9 + .../react/src/routes/sec-b.index.tsx | 9 + .../react/src/routes/sec-b.settings.tsx | 9 + .../react/src/routes/sec-b.tsx | 9 + .../react/src/routes/sec-c.$id.tsx | 11 + .../react/src/routes/sec-c.about.tsx | 9 + .../react/src/routes/sec-c.index.tsx | 9 + .../react/src/routes/sec-c.settings.tsx | 9 + .../react/src/routes/sec-c.tsx | 9 + .../react/src/routes/sec-d.$id.tsx | 11 + .../react/src/routes/sec-d.about.tsx | 9 + .../react/src/routes/sec-d.index.tsx | 9 + .../react/src/routes/sec-d.settings.tsx | 9 + .../react/src/routes/sec-d.tsx | 9 + .../react/src/routes/sec-e.$id.tsx | 11 + .../react/src/routes/sec-e.about.tsx | 9 + .../react/src/routes/sec-e.index.tsx | 9 + .../react/src/routes/sec-e.settings.tsx | 9 + .../react/src/routes/sec-e.tsx | 9 + .../react/src/routes/sec-f.$id.tsx | 11 + .../react/src/routes/sec-f.about.tsx | 9 + .../react/src/routes/sec-f.index.tsx | 9 + .../react/src/routes/sec-f.settings.tsx | 9 + .../react/src/routes/sec-f.tsx | 9 + .../route-tree-scale/react/tsconfig.json | 21 + .../route-tree-scale/react/vite.config.ts | 40 + .../scenarios/route-tree-scale/shared.ts | 61 ++ .../route-tree-scale/solid/project.json | 40 + .../scenarios/route-tree-scale/solid/setup.ts | 17 + .../route-tree-scale/solid/speed.bench.ts | 34 + .../route-tree-scale/solid/speed.flame.ts | 18 + .../route-tree-scale/solid/src/main.tsx | 27 + .../solid/src/routeTree.gen.ts | 881 ++++++++++++++++++ .../solid/src/routes/(marketing)/pricing.tsx | 9 + .../solid/src/routes/(marketing)/promo.tsx | 9 + .../solid/src/routes/__root.tsx | 46 + .../solid/src/routes/_layout.alpha.tsx | 9 + .../solid/src/routes/_layout.beta.tsx | 9 + .../solid/src/routes/_layout.tsx | 9 + .../solid/src/routes/files.$.tsx | 11 + .../solid/src/routes/index.tsx | 9 + .../solid/src/routes/release.v{$version}.tsx | 11 + .../solid/src/routes/sec-a.$id.tsx | 11 + .../solid/src/routes/sec-a.about.tsx | 9 + .../solid/src/routes/sec-a.index.tsx | 9 + .../solid/src/routes/sec-a.settings.tsx | 9 + .../solid/src/routes/sec-a.tsx | 9 + .../solid/src/routes/sec-b.$id.tsx | 11 + .../solid/src/routes/sec-b.about.tsx | 9 + .../solid/src/routes/sec-b.index.tsx | 9 + .../solid/src/routes/sec-b.settings.tsx | 9 + .../solid/src/routes/sec-b.tsx | 9 + .../solid/src/routes/sec-c.$id.tsx | 11 + .../solid/src/routes/sec-c.about.tsx | 9 + .../solid/src/routes/sec-c.index.tsx | 9 + .../solid/src/routes/sec-c.settings.tsx | 9 + .../solid/src/routes/sec-c.tsx | 9 + .../solid/src/routes/sec-d.$id.tsx | 11 + .../solid/src/routes/sec-d.about.tsx | 9 + .../solid/src/routes/sec-d.index.tsx | 9 + .../solid/src/routes/sec-d.settings.tsx | 9 + .../solid/src/routes/sec-d.tsx | 9 + .../solid/src/routes/sec-e.$id.tsx | 11 + .../solid/src/routes/sec-e.about.tsx | 9 + .../solid/src/routes/sec-e.index.tsx | 9 + .../solid/src/routes/sec-e.settings.tsx | 9 + .../solid/src/routes/sec-e.tsx | 9 + .../solid/src/routes/sec-f.$id.tsx | 11 + .../solid/src/routes/sec-f.about.tsx | 9 + .../solid/src/routes/sec-f.index.tsx | 9 + .../solid/src/routes/sec-f.settings.tsx | 9 + .../solid/src/routes/sec-f.tsx | 9 + .../route-tree-scale/solid/tsconfig.json | 21 + .../route-tree-scale/solid/vite.config.ts | 48 + .../route-tree-scale/vue/project.json | 40 + .../scenarios/route-tree-scale/vue/setup.ts | 17 + .../route-tree-scale/vue/speed.bench.ts | 34 + .../route-tree-scale/vue/speed.flame.ts | 18 + .../route-tree-scale/vue/src/main.tsx | 34 + .../route-tree-scale/vue/src/routeTree.gen.ts | 881 ++++++++++++++++++ .../vue/src/routes/(marketing)/pricing.tsx | 12 + .../vue/src/routes/(marketing)/promo.tsx | 12 + .../vue/src/routes/__root.tsx | 53 ++ .../vue/src/routes/_layout.alpha.tsx | 12 + .../vue/src/routes/_layout.beta.tsx | 12 + .../vue/src/routes/_layout.tsx | 12 + .../vue/src/routes/files.$.tsx | 16 + .../route-tree-scale/vue/src/routes/index.tsx | 12 + .../vue/src/routes/release.v{$version}.tsx | 16 + .../vue/src/routes/sec-a.$id.tsx | 16 + .../vue/src/routes/sec-a.about.tsx | 12 + .../vue/src/routes/sec-a.index.tsx | 12 + .../vue/src/routes/sec-a.settings.tsx | 12 + .../route-tree-scale/vue/src/routes/sec-a.tsx | 12 + .../vue/src/routes/sec-b.$id.tsx | 16 + .../vue/src/routes/sec-b.about.tsx | 12 + .../vue/src/routes/sec-b.index.tsx | 12 + .../vue/src/routes/sec-b.settings.tsx | 12 + .../route-tree-scale/vue/src/routes/sec-b.tsx | 12 + .../vue/src/routes/sec-c.$id.tsx | 16 + .../vue/src/routes/sec-c.about.tsx | 12 + .../vue/src/routes/sec-c.index.tsx | 12 + .../vue/src/routes/sec-c.settings.tsx | 12 + .../route-tree-scale/vue/src/routes/sec-c.tsx | 12 + .../vue/src/routes/sec-d.$id.tsx | 16 + .../vue/src/routes/sec-d.about.tsx | 12 + .../vue/src/routes/sec-d.index.tsx | 12 + .../vue/src/routes/sec-d.settings.tsx | 12 + .../route-tree-scale/vue/src/routes/sec-d.tsx | 12 + .../vue/src/routes/sec-e.$id.tsx | 16 + .../vue/src/routes/sec-e.about.tsx | 12 + .../vue/src/routes/sec-e.index.tsx | 12 + .../vue/src/routes/sec-e.settings.tsx | 12 + .../route-tree-scale/vue/src/routes/sec-e.tsx | 12 + .../vue/src/routes/sec-f.$id.tsx | 16 + .../vue/src/routes/sec-f.about.tsx | 12 + .../vue/src/routes/sec-f.index.tsx | 12 + .../vue/src/routes/sec-f.settings.tsx | 12 + .../route-tree-scale/vue/src/routes/sec-f.tsx | 12 + .../route-tree-scale/vue/tsconfig.json | 21 + .../route-tree-scale/vue/vite.config.ts | 42 + .../search-params/react/project.json | 40 + .../scenarios/search-params/react/setup.ts | 17 + .../search-params/react/speed.bench.ts | 34 + .../search-params/react/speed.flame.ts | 18 + .../search-params/react/src/main.tsx | 31 + .../search-params/react/src/routeTree.gen.ts | 95 ++ .../search-params/react/src/routes/__root.tsx | 116 +++ .../react/src/routes/catalog.tsx | 42 + .../search-params/react/src/routes/index.tsx | 14 + .../react/src/routes/products.tsx | 91 ++ .../search-params/react/tsconfig.json | 21 + .../search-params/react/vite.config.ts | 40 + .../scenarios/search-params/shared.ts | 200 ++++ .../search-params/solid/project.json | 40 + .../scenarios/search-params/solid/setup.ts | 17 + .../search-params/solid/speed.bench.ts | 34 + .../search-params/solid/speed.flame.ts | 18 + .../search-params/solid/src/main.tsx | 28 + .../search-params/solid/src/routeTree.gen.ts | 95 ++ .../search-params/solid/src/routes/__root.tsx | 120 +++ .../solid/src/routes/catalog.tsx | 50 + .../search-params/solid/src/routes/index.tsx | 14 + .../solid/src/routes/products.tsx | 97 ++ .../search-params/solid/tsconfig.json | 21 + .../search-params/solid/vite.config.ts | 48 + .../scenarios/search-params/vue/project.json | 40 + .../scenarios/search-params/vue/setup.ts | 17 + .../search-params/vue/speed.bench.ts | 34 + .../search-params/vue/speed.flame.ts | 18 + .../scenarios/search-params/vue/src/main.tsx | 35 + .../search-params/vue/src/routeTree.gen.ts | 95 ++ .../search-params/vue/src/routes/__root.tsx | 114 +++ .../search-params/vue/src/routes/catalog.tsx | 49 + .../search-params/vue/src/routes/index.tsx | 17 + .../search-params/vue/src/routes/products.tsx | 108 +++ .../scenarios/search-params/vue/tsconfig.json | 21 + .../search-params/vue/vite.config.ts | 42 + benchmarks/client-nav/solid/vite.config.ts | 8 +- benchmarks/client-nav/vitest.config.ts | 3 + benchmarks/client-nav/vitest.react.config.ts | 8 + benchmarks/client-nav/vitest.solid.config.ts | 8 + benchmarks/client-nav/vitest.vue.config.ts | 8 + benchmarks/client-nav/vue/vite.config.ts | 8 +- pnpm-lock.yaml | 3 + 590 files changed, 23493 insertions(+), 13 deletions(-) create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/project.json create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/shared.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/project.json create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/shared.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/harness.ts create mode 100644 benchmarks/client-nav/scenarios/head/react/project.json create mode 100644 benchmarks/client-nav/scenarios/head/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/head/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/head/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/head/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx create mode 100644 benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx create mode 100644 benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx create mode 100644 benchmarks/client-nav/scenarios/head/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/head/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/head/shared.ts create mode 100644 benchmarks/client-nav/scenarios/head/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/head/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/head/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/head/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx create mode 100644 benchmarks/client-nav/scenarios/head/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/head/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/head/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/head/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/head/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/head/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx create mode 100644 benchmarks/client-nav/scenarios/head/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/head/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/history/react/project.json create mode 100644 benchmarks/client-nav/scenarios/history/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/history/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/history/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/history/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx create mode 100644 benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx create mode 100644 benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx create mode 100644 benchmarks/client-nav/scenarios/history/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/history/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/history/shared.ts create mode 100644 benchmarks/client-nav/scenarios/history/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/history/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/history/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/history/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/history/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx create mode 100644 benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx create mode 100644 benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx create mode 100644 benchmarks/client-nav/scenarios/history/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/history/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/history/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/history/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/history/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/history/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/history/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx create mode 100644 benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx create mode 100644 benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx create mode 100644 benchmarks/client-nav/scenarios/history/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/history/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/links/react/project.json create mode 100644 benchmarks/client-nav/scenarios/links/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/links/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/links/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/links/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx create mode 100644 benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/links/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/links/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/links/shared.ts create mode 100644 benchmarks/client-nav/scenarios/links/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/links/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/links/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/links/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/links/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx create mode 100644 benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/links/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/links/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/links/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/links/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/links/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/links/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/links/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx create mode 100644 benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/links/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/links/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/react/project.json create mode 100644 benchmarks/client-nav/scenarios/loaders/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/loaders/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/shared.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/mount/react/project.json create mode 100644 benchmarks/client-nav/scenarios/mount/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/mount/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/mount/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/mount/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/mount/shared.ts create mode 100644 benchmarks/client-nav/scenarios/mount/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/mount/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/mount/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/mount/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/mount/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx create mode 100644 benchmarks/client-nav/scenarios/mount/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/mount/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/project.json create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/shared.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/preload/react/project.json create mode 100644 benchmarks/client-nav/scenarios/preload/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/preload/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/preload/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/preload/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/preload/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/preload/shared.ts create mode 100644 benchmarks/client-nav/scenarios/preload/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/preload/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/preload/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/preload/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/preload/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/preload/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/preload/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx create mode 100644 benchmarks/client-nav/scenarios/preload/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/preload/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/project.json create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/shared.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/react/project.json create mode 100644 benchmarks/client-nav/scenarios/search-params/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/search-params/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/shared.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts create mode 100644 benchmarks/client-nav/vitest.react.config.ts create mode 100644 benchmarks/client-nav/vitest.solid.config.ts create mode 100644 benchmarks/client-nav/vitest.vue.config.ts diff --git a/benchmarks/client-nav/README.md b/benchmarks/client-nav/README.md index 44b58107e3..3da0e70f5e 100644 --- a/benchmarks/client-nav/README.md +++ b/benchmarks/client-nav/README.md @@ -1,16 +1,78 @@ # Client Navigation Benchmarks -Cross-framework client-side navigation benchmarks for: +Cross-framework client-side CPU benchmarks for: - `@tanstack/react-router` - `@tanstack/solid-router` - `@tanstack/vue-router` +The benchmarks run in jsdom against production builds of real apps, and are +tracked in CI by CodSpeed (simulation mode). + +> **Scope:** these benchmarks cover the standalone client router only. The +> client side of TanStack Start (hydration of a server-rendered document, +> streamed payload consumption, server-function calls from the client, ...) is +> not covered here; the server side of Start is covered by `benchmarks/ssr`. + ## Layout -- `react/` - React benchmark + Vitest config -- `solid/` - Solid benchmark + Vitest config -- `vue/` - Vue benchmark + Vitest config +- `react/`, `solid/`, `vue/` - baseline benchmark (mixed navigation loop) + Vitest config +- `vitest.react.config.ts`, `vitest.solid.config.ts`, `vitest.vue.config.ts` - per-framework aggregate configs that run the baseline first, then scenario projects +- `scenarios/harness.ts` - shared scenario runner (mount, link-click steps, `onRendered` synchronization) +- `scenarios//shared.ts` - framework-agnostic scenario definition (workload data, step sequence, assertions, bench options) +- `scenarios///` - isolated scenario apps + +Scenario app layout: + +```text +scenarios/// + vite.config.ts + speed.bench.ts + speed.flame.ts + setup.ts + project.json + tsconfig.json + src/ + main.tsx + routeTree.gen.ts + routes/ +``` + +Scenario apps use file-based routing (`@tanstack/router-plugin`) with a +generated `routeTree.gen.ts`, like a regular user app. Each scenario uses one +app per framework instead of sharing routes in the baseline app. This keeps +route-tree size and router options isolated so one scenario cannot shift +another scenario's numbers. The existing baseline apps and bench names stay +stable for CodSpeed continuity. + +## Scenario Responsibilities + +Each scenario isolates one client-side responsibility so benchmark changes can +be attributed to a specific feature area. + +| Scenario | Client-side responsibility | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `react/`, `solid/`, `vue/` baseline apps | Mixed navigation loop: path params, search params, route context, and `useParams`/`useSearch`/`useLoaderData` selector subscriptions. | +| `async-pipeline` | The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async `beforeLoad` context, and parallel nested async loaders. Component-level `Await`/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark. | +| `control-flow` | Loader-thrown `redirect` (including a 2-hop chain), `notFound()` with `notFoundComponent`, loader errors with `errorComponent`, and boundary reset on recovery navigation. | +| `head` | `HeadContent` per-navigation work: nested route `head()` evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation. | +| `history` | History push/replace/back/forward traversal, location masking, registered-but-never-blocking `useBlocker`, and `useCanGoBack`/`useLocation` subscriptions. | +| `links` | Per-navigation cost of ~200 mounted ``s: link prop building, active-state recompute across `activeOptions` variants, `activeProps` swaps, and `MatchRoute`/`useMatchRoute`. | +| `loaders` | Client loader dispatch: always-stale re-runs (`staleTime: 0`), pure cache hits, `loaderDeps`-keyed caching, `router.invalidate()`, and `useLoaderData` selectors. | +| `mount` | Cold start: `createRouter` (route-tree processing) + first render + initial `router.load()` + unmount, with a fresh router per mount. The only scenario measuring router creation. | +| `nested-params` | Deep nesting (8 dynamic levels): per-level `params.parse`/`stringify`, `beforeLoad` context accumulation across matches, and per-level `useParams`/`useRouteContext` subscriptions. | +| `preload` | Intent preloading from hover events, programmatic `router.preloadRoute`, deterministic preload cache behavior (`defaultPreloadStaleTime: 0`), and commit-time cache maintenance. | +| `route-tree-scale` | Route matching and link-target resolution on a wide (~40 route) tree mixing static, dynamic, prefixed-param, splat, pathless-layout, and route-group paths. | +| `search-params` | `validateSearch` execution, search middlewares (`retainSearchParams`/`stripSearchParams`), functional search updaters, structural sharing, and `useSearch` selector subscriptions. | + +## Conventions + +- Apps are built with `NODE_ENV=production` (`minify: false`) into `dist/app.js`; benches import the built bundle, so production package builds and production JSX output are measured, not dev transforms. +- Scenarios behave like a real user app: navigation happens through `` clicks dispatched on real anchor elements (unless a scenario specifically measures the imperative API), the router uses the default browser history, and `scrollRestoration` is enabled. +- Each benchmark iteration advances a fixed, circular sequence of steps; every step awaits the router's `onRendered` event, so render work is included and steps cannot overlap. No two consecutive steps may target the same location, and the sequence ends back on the initial route. +- Setup runs one warm-up lap through the sequence and asserts each step's observable output (e.g. `document.title`), so a scenario that silently stops doing its work fails instead of reporting a fast time. +- Determinism: no wall-clock timers (async work is resolved promises or counted `setTimeout(0)` hops), `staleTime`/`gcTime` only ever `0` or effectively-infinite, no `Math.random`/`Date.now` in the measured loop. +- Calibration: pick per-iteration step counts so a bench's `vitest bench` run stays roughly between 8 and 30 seconds (long enough to average out, short enough for CI). ## Run @@ -20,7 +82,7 @@ Run all benchmarks through Nx so dependency builds are part of the graph: CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf --outputStyle=stream --skipRemoteCache ``` -Run framework-specific benchmarks: +Run framework-specific benchmarks (baseline + all scenarios): ```bash CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:react --outputStyle=stream --skipRemoteCache @@ -28,15 +90,25 @@ CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:solid --output CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:vue --outputStyle=stream --skipRemoteCache ``` +Run a single scenario app manually (after building it through Nx): + +```bash +CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav--:build:client --outputStyle=stream --skipRemoteCache +cd benchmarks/client-nav && NODE_ENV=production vitest bench --config ./scenarios///vite.config.ts +``` + Run framework-specific flame benchmarks (10 second loop, profiled with `@platformatic/flame`, forced to `NODE_ENV=production`): ```bash +# baseline CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:react --outputStyle=stream --skipRemoteCache CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:solid --outputStyle=stream --skipRemoteCache CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:vue --outputStyle=stream --skipRemoteCache +# scenarios +CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav--:test:flame --outputStyle=stream --skipRemoteCache ``` -Typecheck benchmark sources: +Typecheck benchmark sources (baseline + scenarios): ```bash CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:types --outputStyle=stream --skipRemoteCache diff --git a/benchmarks/client-nav/jsdom.ts b/benchmarks/client-nav/jsdom.ts index 05535e693d..855c2a2316 100644 --- a/benchmarks/client-nav/jsdom.ts +++ b/benchmarks/client-nav/jsdom.ts @@ -30,6 +30,11 @@ setGlobal('MutationObserver', window.MutationObserver) setGlobal('sessionStorage', window.sessionStorage) setGlobal('localStorage', window.localStorage) setGlobal('getComputedStyle', window.getComputedStyle.bind(window)) +setGlobal('addEventListener', window.addEventListener.bind(window)) +setGlobal('removeEventListener', window.removeEventListener.bind(window)) +setGlobal('dispatchEvent', window.dispatchEvent.bind(window)) +setGlobal('CustomEvent', window.CustomEvent) +setGlobal('Event', window.Event) setGlobal( 'requestAnimationFrame', @@ -45,5 +50,6 @@ setGlobal( ) window.scrollTo = () => {} +setGlobal('scrollTo', () => {}) export { window } diff --git a/benchmarks/client-nav/package.json b/benchmarks/client-nav/package.json index cb86e70654..73ad3ef5aa 100644 --- a/benchmarks/client-nav/package.json +++ b/benchmarks/client-nav/package.json @@ -10,9 +10,9 @@ "test:flame:solid": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./solid/speed.flame.ts", "test:flame:vue": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./vue/speed.flame.ts", "test:perf": "NODE_ENV=production vitest bench --config ./vitest.config.ts", - "test:perf:react": "NODE_ENV=production vitest bench --config ./react/vite.config.ts ./react/speed.bench.ts", - "test:perf:solid": "NODE_ENV=production vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.ts", - "test:perf:vue": "NODE_ENV=production vitest bench --config ./vue/vite.config.ts ./vue/speed.bench.ts", + "test:perf:react": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts", + "test:perf:solid": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts", + "test:perf:vue": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts", "test:types": "pnpm run test:types:react && pnpm run test:types:solid && pnpm run test:types:vue", "test:types:react": "tsc -p ./react/tsconfig.json --noEmit", "test:types:solid": "tsc -p ./solid/tsconfig.json --noEmit", @@ -31,6 +31,7 @@ "devDependencies": { "@platformatic/flame": "^1.6.0", "@codspeed/vitest-plugin": "^5.5.0", + "@tanstack/router-plugin": "workspace:^", "@testing-library/react": "^16.2.0", "@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-vue": "^6.0.5", @@ -51,6 +52,22 @@ "@tanstack/react-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-react", + "@benchmarks/client-nav-control-flow-react", + "@benchmarks/client-nav-head-react", + "@benchmarks/client-nav-history-react", + "@benchmarks/client-nav-links-react", + "@benchmarks/client-nav-loaders-react", + "@benchmarks/client-nav-mount-react", + "@benchmarks/client-nav-nested-params-react", + "@benchmarks/client-nav-preload-react", + "@benchmarks/client-nav-route-tree-scale-react", + "@benchmarks/client-nav-search-params-react" + ], + "target": "build:client" } ] }, @@ -62,6 +79,22 @@ "@tanstack/solid-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-solid", + "@benchmarks/client-nav-control-flow-solid", + "@benchmarks/client-nav-head-solid", + "@benchmarks/client-nav-history-solid", + "@benchmarks/client-nav-links-solid", + "@benchmarks/client-nav-loaders-solid", + "@benchmarks/client-nav-mount-solid", + "@benchmarks/client-nav-nested-params-solid", + "@benchmarks/client-nav-preload-solid", + "@benchmarks/client-nav-route-tree-scale-solid", + "@benchmarks/client-nav-search-params-solid" + ], + "target": "build:client" } ] }, @@ -73,6 +106,22 @@ "@tanstack/vue-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-vue", + "@benchmarks/client-nav-control-flow-vue", + "@benchmarks/client-nav-head-vue", + "@benchmarks/client-nav-history-vue", + "@benchmarks/client-nav-links-vue", + "@benchmarks/client-nav-loaders-vue", + "@benchmarks/client-nav-mount-vue", + "@benchmarks/client-nav-nested-params-vue", + "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-route-tree-scale-vue", + "@benchmarks/client-nav-search-params-vue" + ], + "target": "build:client" } ] }, @@ -123,7 +172,45 @@ "test:types": { "cache": false, "dependsOn": [ - "^build" + "^build", + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-react", + "@benchmarks/client-nav-async-pipeline-solid", + "@benchmarks/client-nav-async-pipeline-vue", + "@benchmarks/client-nav-control-flow-react", + "@benchmarks/client-nav-control-flow-solid", + "@benchmarks/client-nav-control-flow-vue", + "@benchmarks/client-nav-head-react", + "@benchmarks/client-nav-head-solid", + "@benchmarks/client-nav-head-vue", + "@benchmarks/client-nav-history-react", + "@benchmarks/client-nav-history-solid", + "@benchmarks/client-nav-history-vue", + "@benchmarks/client-nav-links-react", + "@benchmarks/client-nav-links-solid", + "@benchmarks/client-nav-links-vue", + "@benchmarks/client-nav-loaders-react", + "@benchmarks/client-nav-loaders-solid", + "@benchmarks/client-nav-loaders-vue", + "@benchmarks/client-nav-mount-react", + "@benchmarks/client-nav-mount-solid", + "@benchmarks/client-nav-mount-vue", + "@benchmarks/client-nav-nested-params-react", + "@benchmarks/client-nav-nested-params-solid", + "@benchmarks/client-nav-nested-params-vue", + "@benchmarks/client-nav-preload-react", + "@benchmarks/client-nav-preload-solid", + "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-route-tree-scale-react", + "@benchmarks/client-nav-route-tree-scale-solid", + "@benchmarks/client-nav-route-tree-scale-vue", + "@benchmarks/client-nav-search-params-react", + "@benchmarks/client-nav-search-params-solid", + "@benchmarks/client-nav-search-params-vue" + ], + "target": "test:types:client" + } ] } } diff --git a/benchmarks/client-nav/react/vite.config.ts b/benchmarks/client-nav/react/vite.config.ts index ffa1d41be8..adc31ddd64 100644 --- a/benchmarks/client-nav/react/vite.config.ts +++ b/benchmarks/client-nav/react/vite.config.ts @@ -1,8 +1,14 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -25,6 +31,6 @@ export default defineConfig({ name: '@benchmarks/client-nav (react)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], }, }) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/project.json b/benchmarks/client-nav/scenarios/async-pipeline/react/project.json new file mode 100644 index 0000000000..44ab55ccfe --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts new file mode 100644 index 0000000000..20423f5663 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'async-pipeline navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx new file mode 100644 index 0000000000..554c41e02d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx @@ -0,0 +1,32 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..2b3336859a --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* 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 NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +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 NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + 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 + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx new file mode 100644 index 0000000000..1cc8566678 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const ids = ['1', '2'] + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..d644376b7b --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) + +function CtxPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx new file mode 100644 index 0000000000..5b6d70e744 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..bc002c303f --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) + +function NestedPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx new file mode 100644 index 0000000000..443f592373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) + +function NestedLayout() { + const data = Route.useLoaderData() + + return ( +
+
{data.value}
+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..4839b0ca6d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) + +function SlowPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts new file mode 100644 index 0000000000..7161c9a13d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav async-pipeline (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/shared.ts b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts new file mode 100644 index 0000000000..c82f8c18ae --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts @@ -0,0 +1,107 @@ +/** + * Shared definition of the `async-pipeline` scenario: the router's async + * load pipeline — fully async loaders (transition-held navigations), async + * `beforeLoad` context building, and parallel nested async loaders. All async + * work resolves through counted 0ms timer hops so the workload is + * deterministic. The three framework apps consume these builders so the + * workload is identical modulo the rendering layer. + * + * Note: component-level deferred consumption (`Await`/Suspense) is + * intentionally NOT covered. React throttles every Suspense reveal (with or + * without a fallback, including `pendingComponent`) by ~300ms of wall-clock + * time, which cannot be made deterministic; the async work is exercised + * through the router's own pipeline instead. + */ +import type { ScenarioStep } from '../harness' + +/** Resolves after exactly `hops` chained 0ms timer hops. */ +export function hopDelay(hops: number): Promise { + let promise = Promise.resolve() + for (let i = 0; i < hops; i++) { + promise = promise.then( + () => new Promise((resolve) => setTimeout(resolve, 0)), + ) + } + return promise +} + +/** Number of 0ms hops every async loader/beforeLoad takes before resolving. */ +export const deferredHops = 2 + +function checksum(seed: number) { + let value = seed | 0 + for (let index = 0; index < 20; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + return value +} + +export function slowStateValue(id: string) { + return `state-${id}-${checksum(Number(id) * 71 + 3)}` +} + +export function ctxSeedValue(id: string) { + return checksum(Number(id) * 13 + 1) +} + +export function ctxStateValue(id: string) { + return `ctx-${id}-${checksum(ctxSeedValue(id) + 5)}` +} + +export function nestedLayoutValue() { + return `layout-${checksum(97)}` +} + +export function nestedStateValue(id: string) { + return `nested-${id}-${checksum(Number(id) * 41 + 11)}` +} + +function markerText(testId: string) { + return ( + document.querySelector(`[data-testid="${testId}"]`)?.textContent ?? + undefined + ) +} + +function step(testId: string, isSettled: () => boolean): ScenarioStep { + return { type: 'click', testId, isSettled } +} + +export const steps: ReadonlyArray = [ + step('go-slow-1', () => markerText('slow-state') === slowStateValue('1')), + step('go-ctx-1', () => markerText('ctx-state') === ctxStateValue('1')), + step( + 'go-nested-1', + () => + markerText('nested-layout') === nestedLayoutValue() && + markerText('nested-state') === nestedStateValue('1'), + ), + step('go-slow-2', () => markerText('slow-state') === slowStateValue('2')), + step('go-ctx-2', () => markerText('ctx-state') === ctxStateValue('2')), + step( + 'go-nested-2', + () => markerText('nested-state') === nestedStateValue('2'), + ), + step('go-home', () => markerText('home-state') === 'home'), +] + +export function assertStepResult(stepIndex: number) { + const current = steps[stepIndex]! + if (typeof current === 'string' || !('isSettled' in current)) { + return + } + if (!current.isSettled!()) { + throw new Error( + `Unexpected document state after async-pipeline step ${stepIndex}`, + ) + } +} + +// Two laps through the 7-step sequence per benchmark iteration. +export const ticksPerIteration = 14 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json b/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json new file mode 100644 index 0000000000..2fce426b1e --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts new file mode 100644 index 0000000000..ec134a6e33 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'async-pipeline navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx new file mode 100644 index 0000000000..61b9030943 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx @@ -0,0 +1,29 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..7c15baf4e9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* 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 NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +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 NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + 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 + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..7d3647fee8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx @@ -0,0 +1,46 @@ +import { For } from 'solid-js' +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const ids = ['1', '2'] + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..e757b57b9d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) + +function CtxPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx new file mode 100644 index 0000000000..12907d440d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..5518ccf498 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) + +function NestedPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx new file mode 100644 index 0000000000..fdc939dc95 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) + +function NestedLayout() { + const data = Route.useLoaderData() + + return ( +
+
{data().value}
+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..4c229407e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) + +function SlowPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts new file mode 100644 index 0000000000..5107c1f056 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav async-pipeline (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json b/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json new file mode 100644 index 0000000000..911bb61e38 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts new file mode 100644 index 0000000000..cd789259d2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'async-pipeline navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx new file mode 100644 index 0000000000..cfd9ab44da --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx @@ -0,0 +1,36 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..ce26dc62d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* 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 NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +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 NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + 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 + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..c477ab7e68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx @@ -0,0 +1,53 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const ids = ['1', '2'] + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..d995f1ff6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx @@ -0,0 +1,32 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +const CtxPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx new file mode 100644 index 0000000000..128e132190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..bbeaaa7dd7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +const NestedPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx new file mode 100644 index 0000000000..763657dfc6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx @@ -0,0 +1,26 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +const NestedLayout = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () => ( +
+
{data.value.value}
+ +
+ ) + }, +}) + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..c9afccba97 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +const SlowPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts new file mode 100644 index 0000000000..e16a8ee828 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav async-pipeline (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/project.json b/benchmarks/client-nav/scenarios/control-flow/react/project.json new file mode 100644 index 0000000000..e3e580911f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/setup.ts b/benchmarks/client-nav/scenarios/control-flow/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts new file mode 100644 index 0000000000..d76218944a --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'control-flow navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx new file mode 100644 index 0000000000..d480d61ba1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx @@ -0,0 +1,32 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + // The scenario intentionally throws loader errors every lap; keep React 19's + // default per-caught-error console reporting out of the measured loop. + const reactRoot = createRoot(container, { onCaughtError: () => {} }) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..86edcc8472 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* 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 TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx new file mode 100644 index 0000000000..63d5feb901 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function LocationMarker() { + const pathname = useLocation({ select: (location) => location.pathname }) + return {pathname} +} + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx new file mode 100644 index 0000000000..2dc4a26486 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { errorMessage } from '../../../shared' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) + +function BrokenError(props: ErrorComponentProps) { + return
{props.error.message}
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx new file mode 100644 index 0000000000..aacba39384 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx new file mode 100644 index 0000000000..db31abe9f3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx new file mode 100644 index 0000000000..24e856ea15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..0c807bb2bc --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) + +function MissingPage() { + const data = Route.useLoaderData() + return
{data.id}
+} + +function MissingNotFound() { + return
missing not found
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx new file mode 100644 index 0000000000..280f65835e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) + +function TargetPage() { + return
target
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts new file mode 100644 index 0000000000..1aa2c1fa35 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav control-flow (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/shared.ts b/benchmarks/client-nav/scenarios/control-flow/shared.ts new file mode 100644 index 0000000000..c4f368bee1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/shared.ts @@ -0,0 +1,91 @@ +/** + * Shared definition of the `control-flow` scenario: loader-thrown redirects + * (including a 2-hop chain), notFound() rendering, loader errors through the + * route error boundary, and recovery navigation resetting the boundary. + */ +import type { ScenarioStep } from '../harness' + +export const errorMessage = 'bench-error' + +export const steps: ReadonlyArray = [ + { + type: 'click', + testId: 'go-hop1', + // The click lands on /hop1 whose loader chains redirects to /target; wait + // until the post-redirect location is committed before finishing the step. + isSettled: () => + document.querySelector('[data-testid="loc"]')?.textContent === '/target', + }, + 'go-missing-ok', + 'go-missing-gone', + 'go-broken', + 'go-target', + 'go-missing-gone', + 'home', +] + +interface StepExpectation { + loc: string + present: ReadonlyArray + absent?: ReadonlyArray + errorText?: string +} + +const expectations: ReadonlyArray = [ + { loc: '/target', present: ['target-state'] }, + { loc: '/missing/exists', present: ['missing-state'] }, + { + loc: '/missing/gone', + present: ['not-found-state'], + absent: ['missing-state'], + }, + { loc: '/broken', present: ['error-state'], errorText: errorMessage }, + { loc: '/target', present: ['target-state'], absent: ['error-state'] }, + { loc: '/missing/gone', present: ['not-found-state'] }, + { loc: '/', present: ['home-state'] }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expectation = expectations[stepIndex % expectations.length]! + + const loc = container.querySelector('[data-testid="loc"]') + if (loc?.textContent !== expectation.loc) { + throw new Error( + `Expected location "${expectation.loc}" after step ${stepIndex}, received "${loc?.textContent ?? ''}"`, + ) + } + + for (const testId of expectation.present) { + const element = container.querySelector(`[data-testid="${testId}"]`) + if (!element) { + throw new Error( + `Expected [data-testid="${testId}"] after step ${stepIndex}`, + ) + } + if ( + expectation.errorText && + !element.textContent?.includes(expectation.errorText) + ) { + throw new Error( + `Expected [data-testid="${testId}"] to include "${expectation.errorText}", received "${element.textContent}"`, + ) + } + } + + for (const testId of expectation.absent ?? []) { + if (container.querySelector(`[data-testid="${testId}"]`)) { + throw new Error( + `Expected [data-testid="${testId}"] to be absent after step ${stepIndex}`, + ) + } + } +} + +// Two laps through the 7-step sequence per benchmark iteration. +export const ticksPerIteration = 14 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/project.json b/benchmarks/client-nav/scenarios/control-flow/solid/project.json new file mode 100644 index 0000000000..2b5154d385 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts b/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts new file mode 100644 index 0000000000..f3cad06d97 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'control-flow navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..44d88e9f8d --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* 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 TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..1bce9e8a3e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function LocationMarker() { + const pathname = useLocation({ select: (location) => location.pathname }) + return {pathname()} +} + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx new file mode 100644 index 0000000000..9931ef9c58 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { errorMessage } from '../../../shared' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) + +function BrokenError(props: ErrorComponentProps) { + return
{props.error.message}
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx new file mode 100644 index 0000000000..1aedb481ec --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx new file mode 100644 index 0000000000..91b601c516 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx new file mode 100644 index 0000000000..cc46764de5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..9c0493fb48 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) + +function MissingPage() { + const data = Route.useLoaderData() + return
{data().id}
+} + +function MissingNotFound() { + return
missing not found
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx new file mode 100644 index 0000000000..87de4e59a7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) + +function TargetPage() { + return
target
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts new file mode 100644 index 0000000000..1c6244d3d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav control-flow (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/project.json b/benchmarks/client-nav/scenarios/control-flow/vue/project.json new file mode 100644 index 0000000000..7370687988 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts b/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts new file mode 100644 index 0000000000..0224291130 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'control-flow navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..60fc846f89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* 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 TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..9695c4b32d --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx @@ -0,0 +1,57 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/vue-router' + +const LocationMarker = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + return () => {pathname.value} + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx new file mode 100644 index 0000000000..c67fa2817e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { errorMessage } from '../../../shared' + +const BrokenError = Vue.defineComponent({ + props: { + error: { type: Error, required: true }, + }, + setup(props) { + return () =>
{props.error.message}
+ }, +}) + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx new file mode 100644 index 0000000000..ed00cefe3f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx new file mode 100644 index 0000000000..f6d4b1c1e0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f3ffc58610 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..022ea656a6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx @@ -0,0 +1,28 @@ +import * as Vue from 'vue' +import { createFileRoute, notFound } from '@tanstack/vue-router' + +const MissingPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + return () =>
{data.value.id}
+ }, +}) + +const MissingNotFound = Vue.defineComponent({ + setup() { + return () =>
missing not found
+ }, +}) + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx new file mode 100644 index 0000000000..4e22d0993a --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const TargetPage = Vue.defineComponent({ + setup() { + return () =>
target
+ }, +}) + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts new file mode 100644 index 0000000000..4694cf69c8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav control-flow (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/harness.ts b/benchmarks/client-nav/scenarios/harness.ts new file mode 100644 index 0000000000..21590d0d55 --- /dev/null +++ b/benchmarks/client-nav/scenarios/harness.ts @@ -0,0 +1,298 @@ +import { getRequiredLink, waitForRequiredLink } from '../setup-helpers' + +export interface ScenarioRouter { + load: () => Promise + subscribe: (eventType: 'onRendered', callback: () => void) => () => void + navigate: (options: any) => Promise + preloadRoute: (options: any) => Promise + invalidate: () => Promise + history: { + back: () => void + forward: () => void + go: (index: number) => void + } +} + +export interface MountedScenarioApp { + router: ScenarioRouter + unmount: () => void +} + +/** + * A single benchmark step. Plain strings are link-click steps (the common + * case). Steps that change the router location (`click`, `navigate`, `back`, + * `forward`, `go`) are awaited through the router's `onRendered` event; steps + * that do background work (`hover`, `preload`, `invalidate`) are awaited + * through a fixed number of 0ms timer hops so the loop stays deterministic. + */ +export type ScenarioStep = + | string + | { + type: 'click' + testId: string + /** + * Optional post-render settle predicate for steps that trigger work + * finishing after `onRendered` (e.g. deferred data resolving through a + * Suspense boundary). The step keeps taking 0ms timer hops until the + * predicate returns true (bounded, throws when exceeded). + */ + isSettled?: () => boolean + } + | { + /** + * Dispatches `mouseover` + `mouseenter` on the link (React synthesizes + * `mouseEnter` from `mouseover`; Solid and Vue listen to the native + * `mouseenter`), which triggers `preload: 'intent'`, then settles. + */ + type: 'hover' + testId: string + settleHops?: number + } + | { type: 'navigate'; getOptions: () => object } + | { type: 'preload'; getOptions: () => object; settleHops?: number } + | { type: 'invalidate'; settleHops?: number } + | { type: 'back' } + | { type: 'forward' } + | { type: 'go'; delta: number } + +const timerHop = () => new Promise((resolve) => setTimeout(resolve, 0)) + +async function settle(hops: number) { + for (let i = 0; i < hops; i++) { + await timerHop() + } +} + +const MAX_SETTLE_HOPS = 100 + +async function settleUntil(isSettled: () => boolean, label: string) { + for (let i = 0; i < MAX_SETTLE_HOPS; i++) { + if (isSettled()) { + return + } + await timerHop() + } + throw new Error( + `Step "${label}" did not settle within ${MAX_SETTLE_HOPS} timer hops`, + ) +} + +export interface ScenarioSetupOptions { + frameworkLabel: string + mount: (container: HTMLElement) => MountedScenarioApp + /** + * Circular sequence of steps advanced by `tick()`. Link steps must target + * links rendered from the start (e.g. in the root layout), no two + * consecutive location-changing steps may target the same location (a + * same-location click never triggers `onRendered`), and the sequence must + * leave the app back on the initial route so the warm-up lap ends in the + * starting state. + */ + steps: ReadonlyArray + /** Sanity check run once per step during the warm-up lap in `before()`. */ + assertAfterStep?: (stepIndex: number, container: HTMLElement) => void +} + +function warnAboutDevMode(frameworkLabel: string) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `client-nav scenario benchmark is running without NODE_ENV=production; ${frameworkLabel} dev overhead will dominate results.`, + ) + } +} + +/** + * Shared scenario runner: mounts the prebuilt app, then advances a circular + * sequence of steps, synchronizing each step so its work is fully part of the + * measured tick and steps cannot overlap. + */ +export function createScenarioSetup(options: ScenarioSetupOptions) { + warnAboutDevMode(options.frameworkLabel) + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let stepIndex = 0 + let next: () => Promise = () => + Promise.reject(new Error('Benchmark not initialized')) + + async function before() { + stepIndex = 0 + window.history.replaceState(null, '', '/') + container = document.createElement('div') + document.body.append(container) + + const { router, unmount: dispose } = options.mount(container) + unmount = dispose + + let resolveRendered: () => void = () => {} + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + const cachedLinks = new Map() + const rendered = () => + new Promise((resolveNext) => { + resolveRendered = resolveNext + }) + + const runStep = async (step: ScenarioStep) => { + if (typeof step === 'string') { + step = { type: 'click', testId: step } + } + + switch (step.type) { + case 'click': { + const renderedPromise = rendered() + getRequiredLink(container!, step.testId, cachedLinks).dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + button: 0, + }), + ) + await renderedPromise + if (step.isSettled) { + await settleUntil(step.isSettled, `click ${step.testId}`) + } + return + } + case 'hover': { + const link = getRequiredLink(container!, step.testId, cachedLinks) + link.dispatchEvent( + new MouseEvent('mouseover', { bubbles: true, cancelable: true }), + ) + link.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: false, cancelable: false }), + ) + await settle(step.settleHops ?? 4) + return + } + case 'navigate': { + const renderedPromise = rendered() + router.navigate(step.getOptions()) + await renderedPromise + return + } + case 'preload': { + await router.preloadRoute(step.getOptions()) + await settle(step.settleHops ?? 2) + return + } + case 'invalidate': { + await router.invalidate() + await settle(step.settleHops ?? 2) + return + } + case 'back': { + const renderedPromise = rendered() + router.history.back() + await renderedPromise + return + } + case 'forward': { + const renderedPromise = rendered() + router.history.forward() + await renderedPromise + return + } + case 'go': { + const renderedPromise = rendered() + router.history.go(step.delta) + await renderedPromise + return + } + } + } + + await router.load() + + const linkTestIds = new Set() + for (const step of options.steps) { + if (typeof step === 'string') { + linkTestIds.add(step) + } else if (step.type === 'click' || step.type === 'hover') { + linkTestIds.add(step.testId) + } + } + for (const testId of linkTestIds) { + await waitForRequiredLink(container, testId, cachedLinks) + } + + // One warm-up lap that also sanity-checks each step's observable output, + // ending back on the initial route so measurement starts from a known state. + for (const [index, step] of options.steps.entries()) { + await runStep(step) + options.assertAfterStep?.(index, container) + } + + next = () => { + const step = options.steps[stepIndex % options.steps.length]! + stepIndex += 1 + return runStep(step) + } + } + + function after() { + unmount?.() + container?.remove() + unsub() + unmount = undefined + container = undefined + } + + function tick() { + return next() + } + + return { + before, + tick, + after, + } +} + +export interface MountLoopSetupOptions { + frameworkLabel: string + mount: (container: HTMLElement) => MountedScenarioApp + /** Test id that must appear in the container before a mount counts as done. */ + readyTestId: string + assertReady?: (container: HTMLElement) => void +} + +/** + * Mount-loop runner for cold-start scenarios: every tick creates a fresh + * container, mounts the app (router creation included), waits for the initial + * render to commit, then unmounts and cleans up. + */ +export function createMountLoopSetup(options: MountLoopSetupOptions) { + warnAboutDevMode(options.frameworkLabel) + + async function tick() { + window.history.replaceState(null, '', '/') + const container = document.createElement('div') + document.body.append(container) + + const { router, unmount } = options.mount(container) + + try { + await router.load() + await settleUntil( + () => + container.querySelector(`[data-testid="${options.readyTestId}"]`) !== + null, + `mount ${options.readyTestId}`, + ) + options.assertReady?.(container) + } finally { + unmount() + container.remove() + } + } + + return { + before: async () => {}, + tick, + after: () => {}, + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/project.json b/benchmarks/client-nav/scenarios/head/react/project.json new file mode 100644 index 0000000000..1becf30fb9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/setup.ts b/benchmarks/client-nav/scenarios/head/react/setup.ts new file mode 100644 index 0000000000..1a28b0ca94 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/react/speed.bench.ts b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts new file mode 100644 index 0000000000..1c4c84d09d --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'head navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/react/speed.flame.ts b/benchmarks/client-nav/scenarios/head/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/main.tsx b/benchmarks/client-nav/scenarios/head/react/src/main.tsx new file mode 100644 index 0000000000..51b72c7673 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/main.tsx @@ -0,0 +1,30 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..5c80a3104e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* 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 SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx new file mode 100644 index 0000000000..3d61dad0e9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx @@ -0,0 +1,59 @@ +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/react-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..6f5b78d60e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) + +function ArticlePage() { + const params = Route.useParams() + + return ( +
+

{articleTitle(params.id)}

+

{articleDescription(params.id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..6ed1935ada --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) + +function DocsSectionPage() { + const params = Route.useParams() + + return ( +
+

{docsTitle(params.section)}

+

{docsDescription(params.section)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx new file mode 100644 index 0000000000..a2da216ddb --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { docsLayoutHead } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) + +function DocsLayout() { + return ( +
+

Docs

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx new file mode 100644 index 0000000000..6ac318452f --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeHead, homeTitle } from '../../../shared' + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeTitle}

+

Landing page

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx new file mode 100644 index 0000000000..cd73902bff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { settingsHead, settingsTitle } from '../../../shared' + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) + +function SettingsPage() { + return ( +
+

{settingsTitle}

+

Account settings

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/tsconfig.json b/benchmarks/client-nav/scenarios/head/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/react/vite.config.ts b/benchmarks/client-nav/scenarios/head/react/vite.config.ts new file mode 100644 index 0000000000..086bd60bb4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav head (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/head/shared.ts b/benchmarks/client-nav/scenarios/head/shared.ts new file mode 100644 index 0000000000..db185d602b --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/shared.ts @@ -0,0 +1,152 @@ +/** + * Shared definition of the `head` scenario: route head() payloads, the click + * sequence, and per-step sanity assertions. The three framework apps consume + * these builders so the workload is identical modulo the rendering layer. + */ + +const siteName = 'Head Bench' + +export const docsSections = ['api', 'guide', 'cli'] as const +export const articleIds = ['1', '2', '3'] as const + +export const homeTitle = `Home | ${siteName}` +export const settingsTitle = `Settings | ${siteName}` + +export function docsTitle(section: string) { + return `Docs ${section} | ${siteName}` +} + +export function docsDescription(section: string) { + return `Documentation for the ${section} section of the head benchmark app.` +} + +export function articleTitle(id: string) { + return `Article ${id} | ${siteName}` +} + +export function articleDescription(id: string) { + return `Long-form article number ${id}, with a description sized like a realistic summary of the page content.` +} + +export function rootHead() { + return { + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: siteName }, + { name: 'description', content: 'Client head-management benchmark' }, + { name: 'theme-color', content: '#111827' }, + { property: 'og:type', content: 'website' }, + { property: 'og:site_name', content: siteName }, + ], + links: [ + { rel: 'icon', href: '/favicon.ico' }, + { rel: 'preconnect', href: 'https://assets.example.com' }, + ], + } +} + +export function homeHead() { + return { + meta: [ + { title: homeTitle }, + { name: 'description', content: 'Landing page of the head benchmark' }, + { property: 'og:title', content: homeTitle }, + ], + links: [{ rel: 'canonical', href: 'https://example.com/' }], + } +} + +export function docsLayoutHead() { + return { + meta: [ + // Overridden by the child section route on every docs navigation, so the + // dedupe path is exercised for both `name` and `property` entries. + { name: 'description', content: 'Documentation hub' }, + { property: 'og:title', content: `Docs | ${siteName}` }, + { name: 'docsearch:index', content: 'head-bench-docs' }, + ], + links: [{ rel: 'search', href: '/docs/opensearch.xml' }], + } +} + +export function docsSectionHead(section: string) { + return { + meta: [ + { title: docsTitle(section) }, + { name: 'description', content: docsDescription(section) }, + { name: 'keywords', content: `docs,${section},router,benchmark` }, + { property: 'og:title', content: docsTitle(section) }, + { property: 'og:description', content: docsDescription(section) }, + { property: 'og:url', content: `https://example.com/docs/${section}` }, + { name: 'twitter:card', content: 'summary' }, + { name: 'twitter:title', content: docsTitle(section) }, + ], + links: [ + { rel: 'canonical', href: `https://example.com/docs/${section}` }, + { rel: 'prev', href: `https://example.com/docs/${section}/intro` }, + ], + } +} + +export function articleHead(id: string) { + return { + meta: [ + { title: articleTitle(id) }, + { name: 'description', content: articleDescription(id) }, + { name: 'author', content: 'Bench Author' }, + { property: 'og:title', content: articleTitle(id) }, + { property: 'og:description', content: articleDescription(id) }, + { property: 'og:url', content: `https://example.com/articles/${id}` }, + { property: 'og:image', content: `https://example.com/covers/${id}.png` }, + { property: 'article:published_time', content: '2026-01-01T00:00:00Z' }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:title', content: articleTitle(id) }, + ], + links: [{ rel: 'canonical', href: `https://example.com/articles/${id}` }], + } +} + +export function settingsHead() { + return { + meta: [ + { title: settingsTitle }, + // Same names as root entries, so the child-wins dedupe path runs. + { name: 'description', content: 'Account settings' }, + { name: 'theme-color', content: '#f9fafb' }, + { name: 'robots', content: 'noindex' }, + ], + links: [], + } +} + +export const steps = [ + { testId: 'go-docs-api', title: docsTitle('api') }, + { testId: 'go-article-1', title: articleTitle('1') }, + { testId: 'go-docs-guide', title: docsTitle('guide') }, + { testId: 'go-settings', title: settingsTitle }, + { testId: 'go-article-2', title: articleTitle('2') }, + { testId: 'go-docs-cli', title: docsTitle('cli') }, + { testId: 'go-article-3', title: articleTitle('3') }, + { testId: 'go-home', title: homeTitle }, +] as const + +export const stepTestIds = steps.map((step) => step.testId) + +export function assertStepResult(stepIndex: number) { + const expected = steps[stepIndex]!.title + if (document.title !== expected) { + throw new Error( + `Expected document.title to be "${expected}" after step ${stepIndex}, received "${document.title}"`, + ) + } +} + +// Two laps through the 8-step sequence per benchmark iteration. +export const ticksPerIteration = 16 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/head/solid/project.json b/benchmarks/client-nav/scenarios/head/solid/project.json new file mode 100644 index 0000000000..2575a19b89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/solid/setup.ts b/benchmarks/client-nav/scenarios/head/solid/setup.ts new file mode 100644 index 0000000000..4ee924acff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts new file mode 100644 index 0000000000..0f6be675c5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'head navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/main.tsx b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..ead6a4436a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* 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 SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..52ca7da373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import { For } from 'solid-js' +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/solid-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..dab11ada18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) + +function ArticlePage() { + const params = Route.useParams() + + return ( +
+

{articleTitle(params().id)}

+

{articleDescription(params().id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..7ab1123e7a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) + +function DocsSectionPage() { + const params = Route.useParams() + + return ( +
+

{docsTitle(params().section)}

+

{docsDescription(params().section)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx new file mode 100644 index 0000000000..7f84b214d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { docsLayoutHead } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) + +function DocsLayout() { + return ( +
+

Docs

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx new file mode 100644 index 0000000000..229eecc8c1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeHead, homeTitle } from '../../../shared' + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeTitle}

+

Landing page

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx new file mode 100644 index 0000000000..88424caef3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { settingsHead, settingsTitle } from '../../../shared' + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) + +function SettingsPage() { + return ( +
+

{settingsTitle}

+

Account settings

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/tsconfig.json b/benchmarks/client-nav/scenarios/head/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/solid/vite.config.ts b/benchmarks/client-nav/scenarios/head/solid/vite.config.ts new file mode 100644 index 0000000000..7b5a1e22b4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav head (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/project.json b/benchmarks/client-nav/scenarios/head/vue/project.json new file mode 100644 index 0000000000..5ad7909d74 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/vue/setup.ts b/benchmarks/client-nav/scenarios/head/vue/setup.ts new file mode 100644 index 0000000000..3dd6bc95a5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts new file mode 100644 index 0000000000..15f9e07143 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'head navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/vue/src/main.tsx b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b115854c26 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* 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 SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..ad87699fef --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import * as Vue from 'vue' +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/vue-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + ) + }, +}) + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..43a6fade58 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +const ArticlePage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{articleTitle(params.value.id)}

+

{articleDescription(params.value.id)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..ebb59d61a2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +const DocsSectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{docsTitle(params.value.section)}

+

{docsDescription(params.value.section)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx new file mode 100644 index 0000000000..feabbc4d4a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { docsLayoutHead } from '../../../shared' + +const DocsLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Docs

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx new file mode 100644 index 0000000000..421478833f --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeHead, homeTitle } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeTitle}

+

Landing page

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx new file mode 100644 index 0000000000..1a519e16c2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { settingsHead, settingsTitle } from '../../../shared' + +const SettingsPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{settingsTitle}

+

Account settings

+
+ ) + }, +}) + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/tsconfig.json b/benchmarks/client-nav/scenarios/head/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/vue/vite.config.ts b/benchmarks/client-nav/scenarios/head/vue/vite.config.ts new file mode 100644 index 0000000000..0ad2067925 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav head (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/react/project.json b/benchmarks/client-nav/scenarios/history/react/project.json new file mode 100644 index 0000000000..a2fe0001be --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/react/setup.ts b/benchmarks/client-nav/scenarios/history/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/react/speed.bench.ts b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts new file mode 100644 index 0000000000..d84f17ebdb --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'history navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/react/speed.flame.ts b/benchmarks/client-nav/scenarios/history/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/main.tsx b/benchmarks/client-nav/scenarios/history/react/src/main.tsx new file mode 100644 index 0000000000..51b72c7673 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/main.tsx @@ -0,0 +1,30 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..61cc83c4c8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx new file mode 100644 index 0000000000..240d8719d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/react-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx new file mode 100644 index 0000000000..55094556a6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) + +function GalleryPage() { + return ( +
+

Gallery

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx new file mode 100644 index 0000000000..932a72f2ef --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..b9c5e2f8d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { pageLabel } from '../../../shared' + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) + +function PageComponent() { + const params = Route.useParams() + + return ( +
+

{pageLabel(params.n)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..793b531c89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { photoLabel } from '../../../shared' + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) + +function PhotoComponent() { + const params = Route.useParams() + + return ( +
+

{photoLabel(params.photoId)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/tsconfig.json b/benchmarks/client-nav/scenarios/history/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/react/vite.config.ts b/benchmarks/client-nav/scenarios/history/react/vite.config.ts new file mode 100644 index 0000000000..61a1b2feef --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav history (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/shared.ts b/benchmarks/client-nav/scenarios/history/shared.ts new file mode 100644 index 0000000000..9ddfa6c104 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/shared.ts @@ -0,0 +1,89 @@ +/** + * Shared definition of the `history` scenario: push/replace/back/forward + * traversal, location masking, blocker registration overhead (the blocker + * never blocks), and useLocation/useCanGoBack subscriptions. + * + * The step lap is designed to keep the history depth stationary: it ends back + * on the initial entry, so the first push of the next lap truncates the stale + * forward tail and the session history never grows past 4 entries. + */ +import type { ScenarioStep } from '../harness' + +export const pageIds = ['1', '2', '3'] as const +export const maskedPhotoId = '42' + +export function pageLabel(n: string) { + return `Page ${n}` +} + +export function photoLabel(id: string) { + return `Photo ${id}` +} + +/** Registered on every navigation, never blocks. */ +export const shouldBlockFn = () => false + +interface HistoryStepExpectation { + step: ScenarioStep + /** Expected router location pathname (the REAL location, even when masked). */ + pathname: string + /** Expected window.location pathname when it differs (location masking). */ + windowPathname?: string + /** Marker test id that must be present in the container after the step. */ + marker?: string +} + +export const stepExpectations: ReadonlyArray = [ + { step: 'p-1', pathname: '/pages/1', marker: 'page-state' }, + { step: 'p-2', pathname: '/pages/2', marker: 'page-state' }, + { + step: 'photo-masked', + pathname: `/photos/${maskedPhotoId}`, + windowPathname: '/gallery', + marker: 'photo-state', + }, + { step: { type: 'back' }, pathname: '/pages/2', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/pages/1', marker: 'page-state' }, + { step: { type: 'forward' }, pathname: '/pages/2', marker: 'page-state' }, + { step: 'p-3-replace', pathname: '/pages/3', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/pages/1', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/', marker: 'home-state' }, +] + +export const steps = stepExpectations.map((expectation) => expectation.step) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = stepExpectations[stepIndex]! + + const loc = container.querySelector('[data-testid="loc"]')?.textContent + if (loc !== expected.pathname) { + throw new Error( + `Expected location marker to be "${expected.pathname}" after step ${stepIndex}, received "${loc}"`, + ) + } + + const windowPathname = expected.windowPathname ?? expected.pathname + if (window.location.pathname !== windowPathname) { + throw new Error( + `Expected window.location.pathname to be "${windowPathname}" after step ${stepIndex}, received "${window.location.pathname}"`, + ) + } + + if ( + expected.marker && + !container.querySelector(`[data-testid="${expected.marker}"]`) + ) { + throw new Error( + `Expected marker "${expected.marker}" to be rendered after step ${stepIndex}`, + ) + } +} + +// Two laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/history/solid/project.json b/benchmarks/client-nav/scenarios/history/solid/project.json new file mode 100644 index 0000000000..6f4bba1ca5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/solid/setup.ts b/benchmarks/client-nav/scenarios/history/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts new file mode 100644 index 0000000000..833b140e6a --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'history navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/main.tsx b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..a6ab2a4fc8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..edf7368929 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/solid-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx new file mode 100644 index 0000000000..b72d1c6195 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) + +function GalleryPage() { + return ( +
+

Gallery

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx new file mode 100644 index 0000000000..44a0b8045a --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..1def6674c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { pageLabel } from '../../../shared' + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) + +function PageComponent() { + const params = Route.useParams() + + return ( +
+

{pageLabel(params().n)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..ab2fe06e3f --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { photoLabel } from '../../../shared' + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) + +function PhotoComponent() { + const params = Route.useParams() + + return ( +
+

{photoLabel(params().photoId)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/tsconfig.json b/benchmarks/client-nav/scenarios/history/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/solid/vite.config.ts b/benchmarks/client-nav/scenarios/history/solid/vite.config.ts new file mode 100644 index 0000000000..de0eda543c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav history (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/project.json b/benchmarks/client-nav/scenarios/history/vue/project.json new file mode 100644 index 0000000000..17fca887ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/vue/setup.ts b/benchmarks/client-nav/scenarios/history/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts new file mode 100644 index 0000000000..98777d7ecd --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'history navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/vue/src/main.tsx b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..cee1b68870 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..7f7dff159c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/vue-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx new file mode 100644 index 0000000000..3a397b1843 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const GalleryPage = Vue.defineComponent({ + setup() { + return () => ( +
+

Gallery

+
+ ) + }, +}) + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx new file mode 100644 index 0000000000..fbc5b957a7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Home

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..560e83ed29 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { pageLabel } from '../../../shared' + +const PageComponent = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{pageLabel(params.value.n)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..186566fe72 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { photoLabel } from '../../../shared' + +const PhotoComponent = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{photoLabel(params.value.photoId)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/tsconfig.json b/benchmarks/client-nav/scenarios/history/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/vue/vite.config.ts b/benchmarks/client-nav/scenarios/history/vue/vite.config.ts new file mode 100644 index 0000000000..58986d6526 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav history (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/react/project.json b/benchmarks/client-nav/scenarios/links/react/project.json new file mode 100644 index 0000000000..52c9df8a68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/react/setup.ts b/benchmarks/client-nav/scenarios/links/react/setup.ts new file mode 100644 index 0000000000..5409df903c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/react/speed.bench.ts b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts new file mode 100644 index 0000000000..86343d5884 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'links navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/react/speed.flame.ts b/benchmarks/client-nav/scenarios/links/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/main.tsx b/benchmarks/client-nav/scenarios/links/react/src/main.tsx new file mode 100644 index 0000000000..51b72c7673 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/main.tsx @@ -0,0 +1,30 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..97ec042c1c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/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 AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +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 + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + 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, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx new file mode 100644 index 0000000000..48588a9ca4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx @@ -0,0 +1,103 @@ +import { + Link, + MatchRoute, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/react-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function StatusPanel() { + const matchRoute = useMatchRoute() + + return ( + + ) +} + +function LinkGrid() { + return ( +
+ {itemIds.map((id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }) => (isActive ? `Item ${id} on` : `Item ${id} off`)} + +
+ ))} +
+ ) +} + +function RootComponent() { + return ( + <> + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx new file mode 100644 index 0000000000..0e4afc9297 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { aboutMarker } from '../../../shared' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return ( +
+

{aboutMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx new file mode 100644 index 0000000000..2b8c83ebab --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..23121edb5c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { itemMarker } from '../../../shared' + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemMarker(params.id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/tsconfig.json b/benchmarks/client-nav/scenarios/links/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/react/vite.config.ts b/benchmarks/client-nav/scenarios/links/react/vite.config.ts new file mode 100644 index 0000000000..9e9cd48895 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav links (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/shared.ts b/benchmarks/client-nav/scenarios/links/shared.ts new file mode 100644 index 0000000000..6e75ea045c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/shared.ts @@ -0,0 +1,133 @@ +/** + * Shared definition of the `links` scenario: a large persistent grid of + * ``s in the root layout so every navigation recomputes link props and + * active state across many activeOptions variants, plus `useMatchRoute` / + * `MatchRoute` consumers. The three framework apps consume these constants so + * the workload is identical modulo the rendering layer. + */ +import type { ScenarioStep } from '../harness' + +// 40 item ids x 5 link variants = 200 links mounted in the root layout. +export const LINK_ITEM_COUNT = 40 +export const LINK_VARIANT_COUNT = 5 + +export const itemIds = Array.from({ length: LINK_ITEM_COUNT }, (_, index) => + String(index + 1), +) + +// Static search object attached to the includeSearch:false variant. +export const variantSearch = { tab: 'specs', page: 2 } + +// Item ids that get a `go-item-` test id on their plain variant link. +export const stepItemIds = ['1', '7', '13', '25', '40'] + +// Ids probed by the useMatchRoute checks in the root StatusPanel. +export const matchProbeIds = [ + '1', + '5', + '7', + '13', + '20', + '25', + '30', + '35', + '40', + '2', +] + +export function itemMarker(id: string) { + return `item-${id}` +} + +export const homeMarker = 'home' +export const aboutMarker = 'about' + +interface StepDef { + testId: string + marker: string + /** Expected number of `.active-link` elements after the step. */ + activeCount: number + /** When one link is active, its href must include this substring. */ + activeHrefPart?: string +} + +export const stepDefs: ReadonlyArray = [ + { + testId: 'go-item-1', + marker: itemMarker('1'), + activeCount: 1, + activeHrefPart: '/items/1', + }, + { + testId: 'go-item-7', + marker: itemMarker('7'), + activeCount: 1, + activeHrefPart: '/items/7', + }, + { testId: 'go-about', marker: aboutMarker, activeCount: 0 }, + { + testId: 'go-item-13', + marker: itemMarker('13'), + activeCount: 1, + activeHrefPart: '/items/13', + }, + { + testId: 'go-item-25', + marker: itemMarker('25'), + activeCount: 1, + activeHrefPart: '/items/25', + }, + { + testId: 'go-item-40', + marker: itemMarker('40'), + activeCount: 1, + activeHrefPart: '/items/40', + }, + { + testId: 'go-item-7', + marker: itemMarker('7'), + activeCount: 1, + activeHrefPart: '/items/7', + }, + { testId: 'go-home', marker: homeMarker, activeCount: 0 }, +] + +export const scenarioSteps: ReadonlyArray = stepDefs.map( + (step) => step.testId, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const step = stepDefs[stepIndex]! + + const marker = container.querySelector('[data-testid="page-state"]') + if (marker?.textContent !== step.marker) { + throw new Error( + `Expected page marker "${step.marker}" after step ${stepIndex}, received "${marker?.textContent}"`, + ) + } + + const activeLinks = container.querySelectorAll('a.active-link') + if (activeLinks.length !== step.activeCount) { + throw new Error( + `Expected ${step.activeCount} active link(s) after step ${stepIndex}, received ${activeLinks.length}`, + ) + } + if (step.activeHrefPart) { + const href = activeLinks[0]!.getAttribute('href') ?? '' + if (!href.includes(step.activeHrefPart)) { + throw new Error( + `Expected the active link href to include "${step.activeHrefPart}" after step ${stepIndex}, received "${href}"`, + ) + } + } +} + +// One lap through the 8-step sequence per benchmark iteration; each step +// recomputes all 200 links plus the match probes. +export const ticksPerIteration = 8 + +export const benchOptions = { + warmupIterations: 50, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/links/solid/project.json b/benchmarks/client-nav/scenarios/links/solid/project.json new file mode 100644 index 0000000000..ef1dab4c50 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/solid/setup.ts b/benchmarks/client-nav/scenarios/links/solid/setup.ts new file mode 100644 index 0000000000..34270fe8a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts new file mode 100644 index 0000000000..0f95d3d3f6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'links navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/main.tsx b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..b61431e40d --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/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 AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +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 + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + 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, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..1c2aac2945 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx @@ -0,0 +1,118 @@ +import { For } from 'solid-js' +import { + Link, + MatchRoute, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/solid-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function StatusPanel() { + const matchRoute = useMatchRoute() + + return ( + + ) +} + +function LinkGrid() { + return ( +
+ + {(id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }) => + isActive ? `Item ${id} on` : `Item ${id} off` + } + +
+ )} +
+
+ ) +} + +function RootComponent() { + return ( + <> + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx new file mode 100644 index 0000000000..a2a71678b7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { aboutMarker } from '../../../shared' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return ( +
+

{aboutMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx new file mode 100644 index 0000000000..72fbfe387a --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..64d84f81f4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { itemMarker } from '../../../shared' + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemMarker(params().id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/tsconfig.json b/benchmarks/client-nav/scenarios/links/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/solid/vite.config.ts b/benchmarks/client-nav/scenarios/links/solid/vite.config.ts new file mode 100644 index 0000000000..9a5b3d138b --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav links (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/project.json b/benchmarks/client-nav/scenarios/links/vue/project.json new file mode 100644 index 0000000000..fe1e68fad3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/vue/setup.ts b/benchmarks/client-nav/scenarios/links/vue/setup.ts new file mode 100644 index 0000000000..f70a154909 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts new file mode 100644 index 0000000000..4303aa91ef --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'links navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/vue/src/main.tsx b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..cee64be43c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/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 AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +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 + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + 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, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..61e34667fc --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx @@ -0,0 +1,122 @@ +import * as Vue from 'vue' +import { + Link, + MatchRoute, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/vue-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +const StatusPanel = Vue.defineComponent({ + setup() { + const matchRoute = useMatchRoute() + const probes = matchProbeIds.map((id) => ({ + id, + match: matchRoute({ to: '/items/$id', params: { id } }), + })) + + return () => ( + + ) + }, +}) + +const LinkGrid = Vue.defineComponent({ + setup() { + return () => ( +
+ {itemIds.map((id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }: { isActive: boolean }) => + isActive ? `Item ${id} on` : `Item ${id} off` + } + +
+ ))} +
+ ) + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx new file mode 100644 index 0000000000..5cc1473bff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { aboutMarker } from '../../../shared' + +const AboutPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{aboutMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f04a9f5796 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeMarker } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..0dc5efa3d3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { itemMarker } from '../../../shared' + +const ItemPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{itemMarker(params.value.id)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/tsconfig.json b/benchmarks/client-nav/scenarios/links/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/vue/vite.config.ts b/benchmarks/client-nav/scenarios/links/vue/vite.config.ts new file mode 100644 index 0000000000..d1cf9f561d --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav links (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/react/project.json b/benchmarks/client-nav/scenarios/loaders/react/project.json new file mode 100644 index 0000000000..d3dbe585d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/setup.ts b/benchmarks/client-nav/scenarios/loaders/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts new file mode 100644 index 0000000000..073169fc45 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'loaders navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx new file mode 100644 index 0000000000..51b72c7673 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx @@ -0,0 +1,30 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..50f0a8863b --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx new file mode 100644 index 0000000000..6309763cf3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx @@ -0,0 +1,70 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..9b954185cc --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx @@ -0,0 +1,55 @@ +import { createFileRoute } from '@tanstack/react-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + void computeChecksum(value) + return null +} + +function CachedPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Cached

+
{`c-${params.id}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx new file mode 100644 index 0000000000..92afe04a21 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx @@ -0,0 +1,65 @@ +import { createFileRoute } from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function DepsSubscriber() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + void computeChecksum(value) + return null +} + +function DepsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Deps

+
{`d-${search.page}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..fb7cbf2f00 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx @@ -0,0 +1,55 @@ +import { createFileRoute } from '@tanstack/react-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + void computeChecksum(value) + return null +} + +function FreshPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Fresh

+
{`f-${params.id}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx new file mode 100644 index 0000000000..9203c00841 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Loaders Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts new file mode 100644 index 0000000000..6fa28bcd35 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav loaders (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/shared.ts b/benchmarks/client-nav/scenarios/loaders/shared.ts new file mode 100644 index 0000000000..f744246160 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/shared.ts @@ -0,0 +1,134 @@ +/** + * Shared definition of the `loaders` scenario: deterministic loader data + * derivation, the click sequence, and per-step sanity assertions. The three + * framework apps consume these builders so the workload is identical modulo + * the rendering layer. + * + * Isolates: client loader dispatch — always-stale re-runs (staleTime 0), + * pure cache hits (staleTime 1e9), loaderDeps-keyed caching, + * router.invalidate(), and useLoaderData selector subscriptions. + */ + +import type { ScenarioStep } from '../harness' + +export function computeChecksum(seed: number) { + let value = Math.trunc(seed) | 0 + + for (let index = 0; index < 40; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + + return value +} + +export interface LoaderItem { + id: string + name: string + value: number + flag: boolean +} + +export function makeItems(seedText: string): Array { + let seed = 0 + for (let index = 0; index < seedText.length; index++) { + seed = (seed * 31 + seedText.charCodeAt(index)) >>> 0 + } + + const items: Array = [] + let value = seed + + for (let index = 0; index < 50; index++) { + value = (value * 1664525 + 1013904223) >>> 0 + items.push({ + id: `${seedText}-${index}`, + name: `Item ${index} of ${seedText}`, + value: value % 100000, + flag: value % 3 === 0, + }) + } + + return items +} + +export function itemsChecksum(items: Array) { + let sum = 0 + for (const item of items) { + sum = (sum + item.value + (item.flag ? 1 : 0)) >>> 0 + } + return sum +} + +export function normalizePage(value: unknown) { + const page = Number(value) + return Number.isFinite(page) && page > 0 ? Math.trunc(page) : 1 +} + +export function freshMarkerText(id: string) { + return `f-${id}-${itemsChecksum(makeItems(`fresh-${id}`))}` +} + +export function cachedMarkerText(id: string) { + return `c-${id}-${itemsChecksum(makeItems(`cached-${id}`))}` +} + +export function depsMarkerText(page: number) { + return `d-${page}-${itemsChecksum(makeItems(`deps-${page}`))}` +} + +export const steps: ReadonlyArray = [ + 'go-fresh-1', + 'go-fresh-2', + 'go-cached-1', + 'go-cached-2', + 'go-deps-1', + 'go-deps-2', + // Invalidates the currently matched routes (the deps route), re-running its + // loader with unchanged deps. Note: this also marks the cached fresh/cached + // matches invalid, so each of those loaders re-runs once per lap on its next + // visit — deterministic, identical work every lap. + { type: 'invalidate' }, + 'go-home', +] + +interface ExpectedState { + testId: string + text?: string +} + +const expectedStates: ReadonlyArray = [ + { testId: 'fresh-state', text: freshMarkerText('1') }, + { testId: 'fresh-state', text: freshMarkerText('2') }, + { testId: 'cached-state', text: cachedMarkerText('1') }, + { testId: 'cached-state', text: cachedMarkerText('2') }, + { testId: 'deps-state', text: depsMarkerText(1) }, + { testId: 'deps-state', text: depsMarkerText(2) }, + // After invalidate the deps loader re-ran with the same deps. + { testId: 'deps-state', text: depsMarkerText(2) }, + { testId: 'home-state' }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = expectedStates[stepIndex]! + const element = container.querySelector(`[data-testid="${expected.testId}"]`) + + if (!element) { + throw new Error( + `Expected marker "${expected.testId}" to exist after step ${stepIndex}`, + ) + } + + if (expected.text !== undefined && element.textContent !== expected.text) { + throw new Error( + `Expected marker "${expected.testId}" to read "${expected.text}" after step ${stepIndex}, received "${element.textContent}"`, + ) + } +} + +// Two laps through the 8-step sequence per benchmark iteration. +export const ticksPerIteration = 16 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/project.json b/benchmarks/client-nav/scenarios/loaders/solid/project.json new file mode 100644 index 0000000000..071e6bc385 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/setup.ts b/benchmarks/client-nav/scenarios/loaders/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts new file mode 100644 index 0000000000..5b9e158bfb --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'loaders navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..bdec400237 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..30350f7907 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..aab6be74dc --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx @@ -0,0 +1,62 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return computeChecksum(value())} /> +} + +function CachedPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Cached

+
{`c-${params().id}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx new file mode 100644 index 0000000000..2f23f2d3da --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx @@ -0,0 +1,72 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function DepsSubscriber() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + return computeChecksum(value())} /> +} + +function DepsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Deps

+
{`d-${search().page}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..acd6a9aa27 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx @@ -0,0 +1,62 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return computeChecksum(value())} /> +} + +function FreshPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Fresh

+
{`f-${params().id}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx new file mode 100644 index 0000000000..7287d47d0a --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Loaders Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts new file mode 100644 index 0000000000..ea913ac578 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav loaders (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/project.json b/benchmarks/client-nav/scenarios/loaders/vue/project.json new file mode 100644 index 0000000000..69bd95837d --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/setup.ts b/benchmarks/client-nav/scenarios/loaders/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts new file mode 100644 index 0000000000..f015fe908b --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'loaders navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..25a82df477 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..b9102a92c4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..a426e35765 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx @@ -0,0 +1,68 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FirstItemSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const CachedPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Cached

+
+ {`c-${params.value.id}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx new file mode 100644 index 0000000000..f987d15d34 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx @@ -0,0 +1,78 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const DepsSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const DepsPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Deps

+
+ {`d-${search.value.page}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..64eec85cfa --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx @@ -0,0 +1,68 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FirstItemSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FreshPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Fresh

+
+ {`f-${params.value.id}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx new file mode 100644 index 0000000000..c9724c90a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Loaders Bench

+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts new file mode 100644 index 0000000000..350fadf387 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav loaders (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/react/project.json b/benchmarks/client-nav/scenarios/mount/react/project.json new file mode 100644 index 0000000000..61839943b3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/react/setup.ts b/benchmarks/client-nav/scenarios/mount/react/setup.ts new file mode 100644 index 0000000000..ed0426c696 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts new file mode 100644 index 0000000000..864e931ed6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mount loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/main.tsx b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx new file mode 100644 index 0000000000..51b72c7673 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx @@ -0,0 +1,30 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..a796abe0ec --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* 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 ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx new file mode 100644 index 0000000000..0deab171d8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx @@ -0,0 +1,41 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx new file mode 100644 index 0000000000..3fedde283b --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return

About

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..f995d95d94 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' +import { articleData } from '../../../shared' + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) + +function ArticlePage() { + const article = Route.useLoaderData() + + return
{`${article.title} (${article.words} words)`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx new file mode 100644 index 0000000000..437187eb4f --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) + +function BlogIndexPage() { + return

All articles

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx new file mode 100644 index 0000000000..ab4fbeb482 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { blogContext } from '../../../shared' + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) + +function BlogLayout() { + return ( +
+

Blog

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx new file mode 100644 index 0000000000..dbc99d8f66 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeItems, readyTestId } from '../../../shared' + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) + +function HomePage() { + const items = Route.useLoaderData() + + return ( +
+

{`Home (${items.length} items)`}

+
    + {items.map((item) => ( +
  • {`${item.label}: ${item.score}`}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx new file mode 100644 index 0000000000..faba50fc72 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) + +function SearchPage() { + const search = Route.useSearch() + + return

{`Results for "${search.q}" page ${search.page}`}

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx new file mode 100644 index 0000000000..1f46d9d885 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) + +function SettingsPage() { + return

Settings

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..7b729a3dca --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' +import { productData } from '../../../shared' + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) + +function ProductPage() { + const product = Route.useLoaderData() + + return
{`${product.name}: ${product.price}`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx new file mode 100644 index 0000000000..dbca94357a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) + +function ShopIndexPage() { + return

All products

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx new file mode 100644 index 0000000000..5243d7bb0e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { shopContext } from '../../../shared' + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) + +function ShopLayout() { + return ( +
+

Shop

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/tsconfig.json b/benchmarks/client-nav/scenarios/mount/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/react/vite.config.ts b/benchmarks/client-nav/scenarios/mount/react/vite.config.ts new file mode 100644 index 0000000000..68ee46738a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav mount (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/shared.ts b/benchmarks/client-nav/scenarios/mount/shared.ts new file mode 100644 index 0000000000..a0e6df2392 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/shared.ts @@ -0,0 +1,72 @@ +/** + * Shared definition of the `mount` scenario: cold start of a moderate, + * realistic app — router creation (processRouteTree), first render, initial + * `router.load()`, and unmount. This is the only scenario where router + * construction is inside the measured loop; the wider tree exists as + * realistic route-processing weight even though only the index route loads. + */ + +export const readyTestId = 'mount-ready' + +export const homeItemCount = 5 + +export function homeItems() { + return Array.from({ length: homeItemCount }, (_, index) => ({ + id: `item-${index}`, + label: `Featured item ${index}`, + score: index * 17 + 3, + })) +} + +export const homeReadyText = `Home (${homeItemCount} items)` + +export function shopContext() { + return { shopSeed: 7 } +} + +export function blogContext() { + return { blogSeed: 13 } +} + +export function productData(productId: string) { + return { + id: productId, + name: `Product ${productId}`, + price: productId.length * 100 + 42, + } +} + +export function articleData(slug: string) { + return { + slug, + title: `Article ${slug}`, + words: slug.length * 250, + } +} + +export function normalizePage(value: unknown) { + const page = Number(value) + return Number.isFinite(page) && page > 0 ? Math.trunc(page) : 1 +} + +export function normalizeQuery(value: unknown) { + return typeof value === 'string' ? value : '' +} + +export function assertReady(container: HTMLElement) { + const marker = container.querySelector(`[data-testid="${readyTestId}"]`) + if (marker?.textContent !== homeReadyText) { + throw new Error( + `Expected mount marker to be "${homeReadyText}", received "${marker?.textContent}"`, + ) + } +} + +// Mounts per benchmark iteration. +export const ticksPerIteration = 2 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/project.json b/benchmarks/client-nav/scenarios/mount/solid/project.json new file mode 100644 index 0000000000..4349570654 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/setup.ts b/benchmarks/client-nav/scenarios/mount/solid/setup.ts new file mode 100644 index 0000000000..810ea38c91 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts new file mode 100644 index 0000000000..a35b95c8bc --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mount loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..dc0acc183d --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* 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 ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..f5c3be2641 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx @@ -0,0 +1,41 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx new file mode 100644 index 0000000000..f74d85aa06 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return

About

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..82caee757a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { articleData } from '../../../shared' + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) + +function ArticlePage() { + const article = Route.useLoaderData() + + return
{`${article().title} (${article().words} words)`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx new file mode 100644 index 0000000000..8e75108aca --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) + +function BlogIndexPage() { + return

All articles

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx new file mode 100644 index 0000000000..1e2cfbd019 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { blogContext } from '../../../shared' + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) + +function BlogLayout() { + return ( +
+

Blog

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx new file mode 100644 index 0000000000..30665ba224 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx @@ -0,0 +1,23 @@ +import { For } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { homeItems, readyTestId } from '../../../shared' + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) + +function HomePage() { + const items = Route.useLoaderData() + + return ( +
+

{`Home (${items().length} items)`}

+
    + + {(item) =>
  • {`${item.label}: ${item.score}`}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx new file mode 100644 index 0000000000..3f8b2d9dd7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) + +function SearchPage() { + const search = Route.useSearch() + + return

{`Results for "${search().q}" page ${search().page}`}

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx new file mode 100644 index 0000000000..c6b95424d9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) + +function SettingsPage() { + return

Settings

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..71b0411d6b --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { productData } from '../../../shared' + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) + +function ProductPage() { + const product = Route.useLoaderData() + + return
{`${product().name}: ${product().price}`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx new file mode 100644 index 0000000000..e33c8779f9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) + +function ShopIndexPage() { + return

All products

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx new file mode 100644 index 0000000000..ff2ca78653 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { shopContext } from '../../../shared' + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) + +function ShopLayout() { + return ( +
+

Shop

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json b/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts b/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts new file mode 100644 index 0000000000..910d332f9c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav mount (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/project.json b/benchmarks/client-nav/scenarios/mount/vue/project.json new file mode 100644 index 0000000000..f2dbd10440 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/setup.ts b/benchmarks/client-nav/scenarios/mount/vue/setup.ts new file mode 100644 index 0000000000..3976e71c51 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts new file mode 100644 index 0000000000..01409d947b --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mount loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..98643d63c9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* 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 ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..5c2b7b52d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx @@ -0,0 +1,44 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx new file mode 100644 index 0000000000..254ecca29c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const AboutPage = Vue.defineComponent({ + setup() { + return () =>

About

+ }, +}) + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..3b2e855a6e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx @@ -0,0 +1,18 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { articleData } from '../../../shared' + +const ArticlePage = Vue.defineComponent({ + setup() { + const article = Route.useLoaderData() + + return () => ( +
{`${article.value.title} (${article.value.words} words)`}
+ ) + }, +}) + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx new file mode 100644 index 0000000000..d3c2bd0305 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const BlogIndexPage = Vue.defineComponent({ + setup() { + return () =>

All articles

+ }, +}) + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx new file mode 100644 index 0000000000..fe9532cc6f --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { blogContext } from '../../../shared' + +const BlogLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Blog

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b0be4d78bb --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx @@ -0,0 +1,27 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeItems, readyTestId } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + const items = Route.useLoaderData() + + return () => ( +
+

{`Home (${items.value.length} items)`}

+
    + {items.value.map((item) => ( +
  • {`${item.label}: ${item.score}`}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx new file mode 100644 index 0000000000..09fdb29c1e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +const SearchPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + + return () => ( +

{`Results for "${search.value.q}" page ${search.value.page}`}

+ ) + }, +}) + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx new file mode 100644 index 0000000000..3fc0485606 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const SettingsPage = Vue.defineComponent({ + setup() { + return () =>

Settings

+ }, +}) + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..897559599a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx @@ -0,0 +1,18 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { productData } from '../../../shared' + +const ProductPage = Vue.defineComponent({ + setup() { + const product = Route.useLoaderData() + + return () => ( +
{`${product.value.name}: ${product.value.price}`}
+ ) + }, +}) + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx new file mode 100644 index 0000000000..1040520f03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const ShopIndexPage = Vue.defineComponent({ + setup() { + return () =>

All products

+ }, +}) + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx new file mode 100644 index 0000000000..184807cfa0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { shopContext } from '../../../shared' + +const ShopLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Shop

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json b/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts b/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts new file mode 100644 index 0000000000..107e1037b6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav mount (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/react/project.json b/benchmarks/client-nav/scenarios/nested-params/react/project.json new file mode 100644 index 0000000000..800f8af9a8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/setup.ts b/benchmarks/client-nav/scenarios/nested-params/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts new file mode 100644 index 0000000000..cb563390fa --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'nested-params navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx new file mode 100644 index 0000000000..51b72c7673 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx @@ -0,0 +1,30 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..95d4b46623 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* 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 LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx new file mode 100644 index 0000000000..ef2c7b1b18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { leafParamSets, midParams } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx new file mode 100644 index 0000000000..24e856ea15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..9789e28040 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,63 @@ +import { createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.h) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxH }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelH() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[ + params.a, + params.b, + params.c, + params.d, + params.e, + params.f, + params.g, + params.h, + ].join('.')} +
+ + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..650b32ce96 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.g) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxG }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelG() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..ca18e3ccd2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.f) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxF }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelF() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..782c1808c7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.e) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxE }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelE() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..a874bbcb63 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,55 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.d) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxD }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelD() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[params.a, params.b, params.c, params.d].join('.')} +
+ + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..94aba79453 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.c) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxC }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelC() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..1960b68276 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.b) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxB }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelB() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx new file mode 100644 index 0000000000..85d1de6f68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.a) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxA }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelA() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts new file mode 100644 index 0000000000..672b1850ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav nested-params (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/shared.ts b/benchmarks/client-nav/scenarios/nested-params/shared.ts new file mode 100644 index 0000000000..be43a3a6e7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/shared.ts @@ -0,0 +1,140 @@ +/** + * Shared definition of the `nested-params` scenario: deep dynamic nesting with + * per-level params.parse/stringify, beforeLoad context accumulation, and + * useParams/useRouteContext subscribers at every level. The three framework + * apps consume these builders so the workload is identical modulo the + * rendering layer. + */ + +export interface LeafParams { + a: string + b: string + c: string + d: string + e: string + f: string + g: string + h: string +} + +const baseParams: LeafParams = { + a: 'alpha', + b: 'bravo', + c: 'charlie', + d: 'delta', + e: 'echo', + f: 'foxtrot', + g: 'golf', + h: 'hotel', +} + +/** + * set 0: base; set 1: only the deepest param differs (only the leaf re-runs); + * set 2: a mid param differs (levels d..h re-run); set 3: the top param + * differs (every level re-runs). + */ +export const leafParamSets: ReadonlyArray = [ + baseParams, + { ...baseParams, h: 'hilton' }, + { ...baseParams, d: 'dover' }, + { ...baseParams, a: 'axel' }, +] + +export const midParams = { + a: baseParams.a, + b: baseParams.b, + c: baseParams.c, + d: baseParams.d, +} + +export function normalizeParam(value: unknown) { + const raw = String(value) + return raw.length > 24 ? raw.slice(0, 24) : raw +} + +export function smallHash(value: string | number) { + const input = `${value}` + let hash = 5381 + for (let index = 0; index < input.length; index++) { + hash = ((hash * 33) ^ input.charCodeAt(index)) >>> 0 + } + return hash +} + +export function leafMarker(params: LeafParams) { + return [ + params.a, + params.b, + params.c, + params.d, + params.e, + params.f, + params.g, + params.h, + ] + .map(normalizeParam) + .join('.') +} + +export function midMarker(params: typeof midParams) { + return [params.a, params.b, params.c, params.d].map(normalizeParam).join('.') +} + +export const steps = [ + 'leaf-1', + 'leaf-2', + 'leaf-3', + 'leaf-4', + 'mid-4', + 'home', +] as const + +function requireText(container: HTMLElement, testId: string, expected: string) { + const element = container.querySelector(`[data-testid="${testId}"]`) + if (!element || element.textContent !== expected) { + throw new Error( + `Expected [data-testid="${testId}"] to render "${expected}", received "${element?.textContent ?? ''}"`, + ) + } +} + +function requireAbsent(container: HTMLElement, testId: string) { + if (container.querySelector(`[data-testid="${testId}"]`)) { + throw new Error(`Expected [data-testid="${testId}"] to be absent`) + } +} + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const step = steps[stepIndex % steps.length]! + switch (step) { + case 'leaf-1': + requireText(container, 'leaf-state', leafMarker(leafParamSets[0]!)) + break + case 'leaf-2': + requireText(container, 'leaf-state', leafMarker(leafParamSets[1]!)) + break + case 'leaf-3': + requireText(container, 'leaf-state', leafMarker(leafParamSets[2]!)) + break + case 'leaf-4': + requireText(container, 'leaf-state', leafMarker(leafParamSets[3]!)) + break + case 'mid-4': + requireAbsent(container, 'leaf-state') + requireText(container, 'mid-state', midMarker(midParams)) + break + case 'home': + requireAbsent(container, 'leaf-state') + requireAbsent(container, 'mid-state') + break + } +} + +// Three laps through the 6-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/project.json b/benchmarks/client-nav/scenarios/nested-params/solid/project.json new file mode 100644 index 0000000000..eb0b91bd6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts b/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts new file mode 100644 index 0000000000..1acf92206c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'nested-params navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..9065c9294f --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* 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 LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6fe7ab3e3d --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +import { leafParamSets, midParams } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx new file mode 100644 index 0000000000..cc46764de5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..7dc55db0e4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.h) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxH }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelH() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[ + params().a, + params().b, + params().c, + params().d, + params().e, + params().f, + params().g, + params().h, + ].join('.')} +
+ + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..d9c6c9429b --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.g) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxG }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelG() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..010f89bc4d --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.f) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxF }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelF() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..08c107f0c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.e) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxE }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelE() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..1b64edd58b --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,64 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.d) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxD }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelD() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[params().a, params().b, params().c, params().d].join('.')} +
+ + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..e509ea82c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.c) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxC }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelC() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..ada853b845 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.b) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxB }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelB() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx new file mode 100644 index 0000000000..5ecf004fae --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.a) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxA }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelA() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts new file mode 100644 index 0000000000..f5c9e13c0f --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav nested-params (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/project.json b/benchmarks/client-nav/scenarios/nested-params/vue/project.json new file mode 100644 index 0000000000..83c234382c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts b/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts new file mode 100644 index 0000000000..9cea79c687 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'nested-params navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a7d1049b49 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* 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 LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..28dd4e847a --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx @@ -0,0 +1,53 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' +import { leafParamSets, midParams } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f3ffc58610 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..756c31e1a9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,86 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.h), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxH, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelH = Vue.defineComponent({ + setup() { + const params = Route.useParams() + return () => ( + <> + + + + +
+ {[ + params.value.a, + params.value.b, + params.value.c, + params.value.d, + params.value.e, + params.value.f, + params.value.g, + params.value.h, + ].join('.')} +
+ + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..d6b9ccde83 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.g), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxG, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelG = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..76ed24904c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.f), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxF, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelF = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..e24d3dc373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.e), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxE, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelE = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..7785638ee7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,83 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.d), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxD, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelD = Vue.defineComponent({ + setup() { + const params = Route.useParams() + return () => ( + <> + + + + +
+ {[ + params.value.a, + params.value.b, + params.value.c, + params.value.d, + ].join('.')} +
+ + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..0d0adc65ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.c), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxC, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelC = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..f013d865af --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.b), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxB, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelB = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx new file mode 100644 index 0000000000..d5764c0985 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.a), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxA, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelA = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts new file mode 100644 index 0000000000..59a4af48ef --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav nested-params (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/react/project.json b/benchmarks/client-nav/scenarios/preload/react/project.json new file mode 100644 index 0000000000..f095228746 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/react/setup.ts b/benchmarks/client-nav/scenarios/preload/react/setup.ts new file mode 100644 index 0000000000..5409df903c --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts new file mode 100644 index 0000000000..0f239245dc --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'preload interaction loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/main.tsx b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx new file mode 100644 index 0000000000..f81532b3a1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx @@ -0,0 +1,33 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..0c76e85974 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/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 DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx new file mode 100644 index 0000000000..b659fe4d26 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx @@ -0,0 +1,32 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { sections } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx new file mode 100644 index 0000000000..1f87530bac --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { docsMarker } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) + +function DocsPage() { + return ( +
+

{docsMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx new file mode 100644 index 0000000000..2b8c83ebab --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..42962b8be7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/react-router' +import { sectionItems } from '../../../shared' + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{`${params.section}:${data.checksum}`}

+
    + {data.items.map((item) => ( +
  • {`${item.name}=${item.value}`}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/tsconfig.json b/benchmarks/client-nav/scenarios/preload/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/react/vite.config.ts b/benchmarks/client-nav/scenarios/preload/react/vite.config.ts new file mode 100644 index 0000000000..ff51686959 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav preload (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/shared.ts b/benchmarks/client-nav/scenarios/preload/shared.ts new file mode 100644 index 0000000000..020f8aeea6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/shared.ts @@ -0,0 +1,117 @@ +/** + * Shared definition of the `preload` scenario: intent preloading via link + * hover, programmatic `router.preloadRoute`, and commit-time preload-cache + * maintenance. The router is configured with `defaultPreload: 'intent'`, + * `defaultPreloadDelay: 0` (no wall-clock debounce timer) and + * `defaultPreloadStaleTime: 0` — the default 30s wall-clock threshold would + * flip cache-hit behavior mid-run; with 0 every hover re-runs the preload + * work, keeping the loop stationary. + * + * Hover-intent preloading was verified manually per adapter (standalone jsdom + * run inspecting `router.state.cachedMatches` after dispatching + * `mouseover` + `mouseenter` on a link): React listens to the synthetic + * mouseEnter (driven by `mouseover`), Solid and Vue listen to the native + * `mouseenter` — the harness hover step dispatches both. + */ +import type { ScenarioStep } from '../harness' + +export const sections = ['a', 'b', 'c', 'd', 'e'] as const + +export const SECTION_ITEM_COUNT = 30 + +/** Deterministic LCG stream seeded from a string — no Math.random. */ +function seededValues(seedText: string, count: number) { + let value = 0 + for (let i = 0; i < seedText.length; i++) { + value = (value * 31 + seedText.charCodeAt(i)) >>> 0 + } + + const values: Array = [] + for (let i = 0; i < count; i++) { + value = (value * 1664525 + 1013904223) >>> 0 + values.push(value % 100_000) + } + return values +} + +export interface SectionItem { + name: string + value: number +} + +export function sectionItems(section: string): Array { + return seededValues(`section-${section}`, SECTION_ITEM_COUNT).map( + (value, index) => ({ + name: `item-${section}-${index}`, + value, + }), + ) +} + +export function sectionChecksum(section: string) { + return sectionItems(section).reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) +} + +export function sectionMarker(section: string) { + return `${section}:${sectionChecksum(section)}` +} + +export const docsMarker = 'docs' +export const homeMarker = 'home' + +interface StepDef { + step: ScenarioStep + /** Expected page marker after the step; undefined for non-navigation steps. */ + marker?: string +} + +export const stepDefs: ReadonlyArray = [ + { step: { type: 'hover', testId: 's-a' } }, + { step: { type: 'hover', testId: 's-b' } }, + { step: 's-a', marker: sectionMarker('a') }, + { step: { type: 'hover', testId: 's-c' } }, + { step: 's-c', marker: sectionMarker('c') }, + { + step: { + type: 'preload', + getOptions: () => ({ + to: '/sections/$section', + params: { section: 'd' }, + }), + }, + }, + { step: 's-d', marker: sectionMarker('d') }, + { step: { type: 'hover', testId: 's-e' } }, + { step: 'go-docs', marker: docsMarker }, + { step: 'go-home', marker: homeMarker }, +] + +export const scenarioSteps: ReadonlyArray = stepDefs.map( + (def) => def.step, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const def = stepDefs[stepIndex]! + if (!def.marker) { + return + } + + const marker = container.querySelector('[data-testid="page-state"]') + if (marker?.textContent !== def.marker) { + throw new Error( + `Expected page marker "${def.marker}" after step ${stepIndex}, received "${marker?.textContent}"`, + ) + } +} + +// Two laps through the 10-step sequence per benchmark iteration. +export const ticksPerIteration = 20 + +export const benchOptions = { + warmupIterations: 50, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/project.json b/benchmarks/client-nav/scenarios/preload/solid/project.json new file mode 100644 index 0000000000..a186879335 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/setup.ts b/benchmarks/client-nav/scenarios/preload/solid/setup.ts new file mode 100644 index 0000000000..34270fe8a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts new file mode 100644 index 0000000000..065cc92a39 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'preload interaction loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx new file mode 100644 index 0000000000..c62ae4a8be --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx @@ -0,0 +1,30 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..645d925238 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/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 DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..a805ed0feb --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx @@ -0,0 +1,34 @@ +import { For } from 'solid-js' +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +import { sections } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx new file mode 100644 index 0000000000..b077cfd18f --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { docsMarker } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) + +function DocsPage() { + return ( +
+

{docsMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx new file mode 100644 index 0000000000..72fbfe387a --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..2a7fb2ad1b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx @@ -0,0 +1,33 @@ +import { For } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { sectionItems } from '../../../shared' + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{`${params().section}:${data().checksum}`}

+
    + + {(item) =>
  • {`${item.name}=${item.value}`}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json b/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts b/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts new file mode 100644 index 0000000000..b542834469 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav preload (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/project.json b/benchmarks/client-nav/scenarios/preload/vue/project.json new file mode 100644 index 0000000000..404476723d --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/setup.ts b/benchmarks/client-nav/scenarios/preload/vue/setup.ts new file mode 100644 index 0000000000..f70a154909 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts new file mode 100644 index 0000000000..66d4120133 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'preload interaction loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx new file mode 100644 index 0000000000..df1e70a83a --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx @@ -0,0 +1,37 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..05a751060b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/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 DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..bf91ba3770 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx @@ -0,0 +1,35 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' +import { sections } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx new file mode 100644 index 0000000000..07d0a989ad --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { docsMarker } from '../../../shared' + +const DocsPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{docsMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f04a9f5796 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeMarker } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..aeb617a86f --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx @@ -0,0 +1,37 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { sectionItems } from '../../../shared' + +const SectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return () => ( +
+

+ {`${params.value.section}:${data.value.checksum}`} +

+
    + {data.value.items.map((item) => ( +
  • {`${item.name}=${item.value}`}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json b/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts b/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts new file mode 100644 index 0000000000..25e64b5bef --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav preload (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json new file mode 100644 index 0000000000..3291738f79 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts new file mode 100644 index 0000000000..47d42c8942 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'route-tree-scale navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx new file mode 100644 index 0000000000..51b72c7673 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx @@ -0,0 +1,30 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..b58b75180f --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* 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 SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..ac2a376549 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) + +function Page() { + return
pricing
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..b71962254e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) + +function Page() { + return
promo
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx new file mode 100644 index 0000000000..31df6ce3fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx @@ -0,0 +1,46 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..893419cef5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) + +function Page() { + return
alpha
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..d20c6007ee --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) + +function Page() { + return
beta
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx new file mode 100644 index 0000000000..961feb50f1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx new file mode 100644 index 0000000000..028aa43d5a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`files:${params._splat}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx new file mode 100644 index 0000000000..8a7ffca9e7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Page, +}) + +function Page() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..dfe8e31905 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`release:${params.version}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..8be053a040 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-a:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..54c1b28618 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) + +function Page() { + return
sec-a:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..3c793a94f5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) + +function Page() { + return
sec-a:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..0037f6463d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) + +function Page() { + return
sec-a:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx new file mode 100644 index 0000000000..846f96b67b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..0dd7f94feb --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-b:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..fde4647472 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) + +function Page() { + return
sec-b:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..51b69cf664 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) + +function Page() { + return
sec-b:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..977b7438b1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) + +function Page() { + return
sec-b:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx new file mode 100644 index 0000000000..77d7e6475d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..eb3be796c0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-c:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..7910acf332 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) + +function Page() { + return
sec-c:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..f6ae102f2d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) + +function Page() { + return
sec-c:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..bf2be30788 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) + +function Page() { + return
sec-c:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx new file mode 100644 index 0000000000..77ac5644c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..d34aee8972 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-d:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..1f095cb2ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) + +function Page() { + return
sec-d:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..8fc698a4d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) + +function Page() { + return
sec-d:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..76c7a178ea --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) + +function Page() { + return
sec-d:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx new file mode 100644 index 0000000000..5f3d281b5e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..6e31d0c5d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-e:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..5a2835917b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) + +function Page() { + return
sec-e:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..0f23b8d484 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) + +function Page() { + return
sec-e:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..b566a1ffaa --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) + +function Page() { + return
sec-e:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx new file mode 100644 index 0000000000..e573a71d98 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..a71c17f96e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-f:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..06831063ad --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) + +function Page() { + return
sec-f:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..4cd54b6a7f --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) + +function Page() { + return
sec-f:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..92bdca5e44 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) + +function Page() { + return
sec-f:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx new file mode 100644 index 0000000000..2fa27873e8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts new file mode 100644 index 0000000000..feb768bfa7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts new file mode 100644 index 0000000000..4e0ba89a0b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts @@ -0,0 +1,61 @@ +/** + * Shared definition of the `route-tree-scale` scenario: route matching, + * location building, and link-target resolution over a wide route tree + * (~40 routes) mixing static segments, dynamic params, prefixed params, + * splats, pathless layouts, and route groups. Navigations jump between + * distant branches so matching cannot reuse the previous branch. + */ + +export const sections = [ + 'sec-a', + 'sec-b', + 'sec-c', + 'sec-d', + 'sec-e', + 'sec-f', +] as const + +export const steps = [ + 'go-sec-a-id', + 'go-sec-f-settings', + 'go-files', + 'go-release', + 'go-alpha', + 'go-promo', + 'go-sec-c-id', + 'go-sec-d-about', + 'go-home', +] as const + +const expectedMarkers = [ + 'sec-a:11', + 'sec-f:settings', + 'files:x/y/z', + 'release:9', + 'alpha', + 'promo', + 'sec-c:42', + 'sec-d:about', + 'home', +] as const + +export function assertStepResult(stepIndex: number) { + const expected = expectedMarkers[stepIndex]! + const actual = document.querySelector( + '[data-testid="scale-state"]', + )?.textContent + if (actual !== expected) { + throw new Error( + `Expected scale-state marker "${expected}" after step ${stepIndex}, received "${actual}"`, + ) + } +} + +// Three laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 27 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json new file mode 100644 index 0000000000..c568e41196 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts new file mode 100644 index 0000000000..20bedc14e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'route-tree-scale navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx new file mode 100644 index 0000000000..54609e9388 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx @@ -0,0 +1,27 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..20b4af6146 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* 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 SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..0965507de6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) + +function Page() { + return
pricing
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..220bd06a68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) + +function Page() { + return
promo
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..3695d0e216 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx @@ -0,0 +1,46 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..234b8488a9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) + +function Page() { + return
alpha
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..39bc716efa --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) + +function Page() { + return
beta
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx new file mode 100644 index 0000000000..f1e499d3b3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx new file mode 100644 index 0000000000..47b1a5eea9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`files:${params()._splat}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx new file mode 100644 index 0000000000..25b1c03c1a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Page, +}) + +function Page() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..0dbcee35ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`release:${params().version}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..18731a2844 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-a:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..1699360a1c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) + +function Page() { + return
sec-a:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..e71e525d07 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) + +function Page() { + return
sec-a:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..49d470190e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) + +function Page() { + return
sec-a:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx new file mode 100644 index 0000000000..2861169fb9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..86fe71d7c0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-b:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..586b829cd0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) + +function Page() { + return
sec-b:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..7098416658 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) + +function Page() { + return
sec-b:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..c2e0f47650 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) + +function Page() { + return
sec-b:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx new file mode 100644 index 0000000000..f2905da9ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..3e07e8033a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-c:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..86ed5043a3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) + +function Page() { + return
sec-c:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..aa9ed2dc6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) + +function Page() { + return
sec-c:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..d0dd3e952c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) + +function Page() { + return
sec-c:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx new file mode 100644 index 0000000000..fa53825238 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..3722012d7c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-d:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..860d2b9e5b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) + +function Page() { + return
sec-d:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..ec7b8eaf2c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) + +function Page() { + return
sec-d:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..d83e3885fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) + +function Page() { + return
sec-d:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx new file mode 100644 index 0000000000..e71391e2a3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..e58d9d77ac --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-e:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..3ef4d2c763 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) + +function Page() { + return
sec-e:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..88bf7ca271 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) + +function Page() { + return
sec-e:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..8a9945926d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) + +function Page() { + return
sec-e:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx new file mode 100644 index 0000000000..8b5ef24519 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..2b81ef96ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-f:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..1c4e3a5474 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) + +function Page() { + return
sec-f:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..65803ee848 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) + +function Page() { + return
sec-f:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..5761138262 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) + +function Page() { + return
sec-f:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx new file mode 100644 index 0000000000..c7f25eecdd --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts new file mode 100644 index 0000000000..24ad9efde1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json new file mode 100644 index 0000000000..f613070038 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts new file mode 100644 index 0000000000..2ff2d5c8b4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'route-tree-scale navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx new file mode 100644 index 0000000000..55507fa1e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx @@ -0,0 +1,34 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a03c051a79 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* 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 SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..f606c10b4b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
pricing
+ }, +}) + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..61c3eb3ec9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
promo
+ }, +}) + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..760e23d3e0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx @@ -0,0 +1,53 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..ff6ad5350e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
alpha
+ }, +}) + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..2f7cab6c0a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
beta
+ }, +}) + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx new file mode 100644 index 0000000000..e8221f0f65 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx new file mode 100644 index 0000000000..221fae01be --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`files:${params.value._splat}`}
+ ) + }, +}) + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b3e94f37c4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..86b1a2c8f0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`release:${params.value.version}`}
+ ) + }, +}) + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..864074940d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-a:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..715ab9548c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:about
+ }, +}) + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..baef08547c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:index
+ }, +}) + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..9e6a56b913 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:settings
+ }, +}) + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx new file mode 100644 index 0000000000..a9fb557aea --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..9a03a8fe01 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-b:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..2ef1100995 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:about
+ }, +}) + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..652e5f895c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:index
+ }, +}) + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..e8fa36377e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:settings
+ }, +}) + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx new file mode 100644 index 0000000000..ba31612633 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..7f0407d84e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-c:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..d26776d439 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:about
+ }, +}) + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..1835eff9b7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:index
+ }, +}) + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..1bf85fd6d9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:settings
+ }, +}) + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx new file mode 100644 index 0000000000..009f814b93 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..ff5aab05e3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-d:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..25a8078626 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:about
+ }, +}) + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..cd9b89664a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:index
+ }, +}) + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..8e4309de32 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:settings
+ }, +}) + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx new file mode 100644 index 0000000000..6b12b514da --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..4525051967 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-e:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..71a32bd36b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:about
+ }, +}) + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..9b467ca53e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:index
+ }, +}) + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..a01066488c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:settings
+ }, +}) + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx new file mode 100644 index 0000000000..38f4da91fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..c740f9d223 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-f:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..229dab22a2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:about
+ }, +}) + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..14e3f46a13 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:index
+ }, +}) + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..da2c7483b9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:settings
+ }, +}) + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx new file mode 100644 index 0000000000..3b389c8144 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts new file mode 100644 index 0000000000..5cd87f517d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/react/project.json b/benchmarks/client-nav/scenarios/search-params/react/project.json new file mode 100644 index 0000000000..f2d743f5f9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/setup.ts b/benchmarks/client-nav/scenarios/search-params/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts new file mode 100644 index 0000000000..9f836fd495 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'search-params navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx new file mode 100644 index 0000000000..0891f819d8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx @@ -0,0 +1,31 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..7380b704e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/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 ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx new file mode 100644 index 0000000000..86399d0175 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx @@ -0,0 +1,116 @@ +import { + Link, + Outlet, + createRootRoute, + useSearch, +} from '@tanstack/react-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +function RootSearchSubscriber() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + void computeChecksum(value) + return null +} + +function RootComponent() { + return ( + <> + {rootSubscribers.map((index) => ( + + ))} + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx new file mode 100644 index 0000000000..73f59c13b0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx @@ -0,0 +1,42 @@ +import { createFileRoute, retainSearchParams } from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function ViewSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + void computeChecksum(value) + return null +} + +function CatalogPage() { + const search = Route.useSearch() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} +

Catalog

+
{catalogMarkerText(search)}
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx new file mode 100644 index 0000000000..5879ad518d --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Search Params Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx new file mode 100644 index 0000000000..d22e99c4bf --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx @@ -0,0 +1,91 @@ +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PageSubscriber() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + void computeChecksum(value) + return null +} + +function FiltersSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + void computeChecksum(value) + return null +} + +function QuerySubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + void computeChecksum(value) + return null +} + +function ProductsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Products

+
{productsMarkerText(search)}
+

{`checksum ${loaderData.checksum}`}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts new file mode 100644 index 0000000000..b220ae7d0d --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav search-params (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/shared.ts b/benchmarks/client-nav/scenarios/search-params/shared.ts new file mode 100644 index 0000000000..343e319ded --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/shared.ts @@ -0,0 +1,200 @@ +/** + * Shared definition of the `search-params` scenario: search schema + * normalizers, the click sequence, and per-step sanity assertions. The three + * framework apps consume these builders so the workload is identical modulo + * the rendering layer. + * + * Isolates: validateSearch execution, search middlewares + * (retainSearchParams/stripSearchParams), parse/stringify of medium-complex + * search objects, functional search updaters, structural sharing, and + * useSearch selector subscriptions. + */ + +import type { ScenarioStep } from '../harness' + +export function computeChecksum(seed: number) { + let value = Math.trunc(seed) | 0 + + for (let index = 0; index < 40; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + + return value +} + +export interface ProductsSearch { + page: number + perPage: number + sort: 'asc' | 'desc' + q: string + filters: { + categories: Array + price: { min: number; max: number } + tags: Array + } +} + +export interface CatalogSearch { + view: 'grid' | 'list' + page: number + perPage: number + sort: 'asc' | 'desc' +} + +function normalizeNumber(value: unknown, fallback: number) { + const num = Number(value) + return Number.isFinite(num) && num >= 0 ? Math.trunc(num) : fallback +} + +function normalizeString(value: unknown, fallback: string) { + return typeof value === 'string' ? value : fallback +} + +function normalizeSort(value: unknown): 'asc' | 'desc' { + return value === 'desc' ? 'desc' : 'asc' +} + +function normalizeStringArray(value: unknown): Array { + if (!Array.isArray(value)) { + return [] + } + return value.filter((entry): entry is string => typeof entry === 'string') +} + +export function normalizeProductsSearch( + search: Record, +): ProductsSearch { + const filters = + typeof search.filters === 'object' && search.filters !== null + ? (search.filters as Record) + : {} + const price = + typeof filters.price === 'object' && filters.price !== null + ? (filters.price as Record) + : {} + + return { + page: Math.max(1, normalizeNumber(search.page, 1)), + perPage: Math.max(1, normalizeNumber(search.perPage, 20)), + sort: normalizeSort(search.sort), + q: normalizeString(search.q, ''), + filters: { + categories: normalizeStringArray(filters.categories), + price: { + min: normalizeNumber(price.min, 0), + max: normalizeNumber(price.max, 1000), + }, + tags: normalizeStringArray(filters.tags), + }, + } +} + +export function normalizeCatalogSearch( + search: Record, +): CatalogSearch { + return { + view: search.view === 'list' ? 'list' : 'grid', + page: Math.max(1, normalizeNumber(search.page, 1)), + perPage: Math.max(1, normalizeNumber(search.perPage, 20)), + sort: normalizeSort(search.sort), + } +} + +export const initialProductsSearch = { + page: 2, + perPage: 50, + sort: 'desc', + q: 'gadget', + filters: { + categories: ['audio', 'video'], + price: { min: 10, max: 200 }, + tags: ['new', 'sale'], + }, +} satisfies ProductsSearch + +export const changedCategories = ['x-ray', 'yield', 'zeta'] + +export function productsMarkerText(search: ProductsSearch) { + return `${search.page}|${search.sort}|${search.filters.categories.join(',')}` +} + +export function catalogMarkerText(search: CatalogSearch) { + return `${search.view}|${search.page}|${search.perPage}|${search.sort}` +} + +export function productsLoaderChecksum(deps: { + page: number + sort: string + filters: ProductsSearch['filters'] +}) { + return computeChecksum( + deps.page * 31 + + deps.sort.length * 7 + + deps.filters.categories.join('').length * 13 + + deps.filters.tags.length * 3 + + (deps.filters.price.max - deps.filters.price.min), + ) +} + +export const steps: ReadonlyArray = [ + 'go-products', + 'products-next-page', + 'products-categories', + 'products-sort', + 'products-reset', + 'go-catalog', + 'catalog-next-page', + 'products-partial', + 'go-home', +] + +interface ExpectedState { + testId: string + text?: string +} + +const expectedStates: ReadonlyArray = [ + // Full search literal from home. + { testId: 'products-state', text: '2|desc|audio,video' }, + // Functional updater bumping the page. + { testId: 'products-state', text: '3|desc|audio,video' }, + // Functional updater replacing filters.categories. + { testId: 'products-state', text: '3|desc|x-ray,yield,zeta' }, + // Functional updater flipping sort desc -> asc. + { testId: 'products-state', text: '3|asc|x-ray,yield,zeta' }, + // Functional updater resetting page to 1 (stripSearchParams removes it). + { testId: 'products-state', text: '1|asc|x-ray,yield,zeta' }, + // search: true carries the current products search into catalog. + { testId: 'catalog-state', text: 'grid|1|50|asc' }, + // Functional updater bumping the catalog page. + { testId: 'catalog-state', text: 'grid|2|50|asc' }, + // Partial search literal; retainSearchParams fills perPage/sort from catalog. + { testId: 'products-state', text: '1|asc|' }, + { testId: 'home-state' }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = expectedStates[stepIndex]! + const element = container.querySelector(`[data-testid="${expected.testId}"]`) + + if (!element) { + throw new Error( + `Expected marker "${expected.testId}" to exist after step ${stepIndex}`, + ) + } + + if (expected.text !== undefined && element.textContent !== expected.text) { + throw new Error( + `Expected marker "${expected.testId}" to read "${expected.text}" after step ${stepIndex}, received "${element.textContent}"`, + ) + } +} + +// Two laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/project.json b/benchmarks/client-nav/scenarios/search-params/solid/project.json new file mode 100644 index 0000000000..af48217e03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/setup.ts b/benchmarks/client-nav/scenarios/search-params/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts new file mode 100644 index 0000000000..9c21ef7a29 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'search-params navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx new file mode 100644 index 0000000000..8e3bf837f5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx @@ -0,0 +1,28 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..96d211d64c --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/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 ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..30c042f9c5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx @@ -0,0 +1,120 @@ +import { createRenderEffect } from 'solid-js' +import { + Link, + Outlet, + createRootRoute, + useSearch, +} from '@tanstack/solid-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function RootSearchSubscriber() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + return computeChecksum(value())} /> +} + +function RootComponent() { + return ( + <> + {rootSubscribers.map(() => ( + + ))} + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx new file mode 100644 index 0000000000..60cf8e7896 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx @@ -0,0 +1,50 @@ +import { createRenderEffect } from 'solid-js' +import { createFileRoute, retainSearchParams } from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function ViewSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + return computeChecksum(value())} /> +} + +function CatalogPage() { + const search = Route.useSearch() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} +

Catalog

+
{catalogMarkerText(search())}
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx new file mode 100644 index 0000000000..0bbb913d83 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Search Params Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx new file mode 100644 index 0000000000..8b2a252155 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx @@ -0,0 +1,97 @@ +import { createRenderEffect } from 'solid-js' +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function PageSubscriber() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + return computeChecksum(value())} /> +} + +function FiltersSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + return computeChecksum(value())} /> +} + +function QuerySubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + return computeChecksum(value())} /> +} + +function ProductsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Products

+
{productsMarkerText(search())}
+

{`checksum ${loaderData().checksum}`}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts new file mode 100644 index 0000000000..c2f4b32806 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav search-params (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/project.json b/benchmarks/client-nav/scenarios/search-params/vue/project.json new file mode 100644 index 0000000000..47b9712a2c --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/setup.ts b/benchmarks/client-nav/scenarios/search-params/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts new file mode 100644 index 0000000000..2fcf07bc8c --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'search-params navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx new file mode 100644 index 0000000000..667228a6c2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx @@ -0,0 +1,35 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b95b667803 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/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 ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..b4cb07f2ae --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx @@ -0,0 +1,114 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute, useSearch } from '@tanstack/vue-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +const RootSearchSubscriber = Vue.defineComponent({ + setup() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + {rootSubscribers.map((index) => ( + + ))} + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx new file mode 100644 index 0000000000..6a09efdd4a --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx @@ -0,0 +1,49 @@ +import * as Vue from 'vue' +import { createFileRoute, retainSearchParams } from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const ViewSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const CatalogPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} +

Catalog

+
{catalogMarkerText(search.value)}
+
+ ) + }, +}) + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx new file mode 100644 index 0000000000..081aa7b4fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Search Params Bench

+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx new file mode 100644 index 0000000000..8be1683e14 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx @@ -0,0 +1,108 @@ +import * as Vue from 'vue' +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const PageSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FiltersSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const QuerySubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const ProductsPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Products

+
+ {productsMarkerText(search.value)} +
+

{`checksum ${loaderData.value.checksum}`}

+
+ ) + }, +}) + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts new file mode 100644 index 0000000000..9a3b82d066 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav search-params (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/solid/vite.config.ts b/benchmarks/client-nav/solid/vite.config.ts index 24db8fe745..bd11fe8de9 100644 --- a/benchmarks/client-nav/solid/vite.config.ts +++ b/benchmarks/client-nav/solid/vite.config.ts @@ -1,8 +1,14 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import solid from 'vite-plugin-solid' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -28,7 +34,7 @@ export default defineConfig({ name: '@benchmarks/client-nav (solid)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], server: { deps: { inline: [/@solidjs/, /@tanstack\/solid-store/], diff --git a/benchmarks/client-nav/vitest.config.ts b/benchmarks/client-nav/vitest.config.ts index 14776452ed..808e7fc724 100644 --- a/benchmarks/client-nav/vitest.config.ts +++ b/benchmarks/client-nav/vitest.config.ts @@ -7,6 +7,9 @@ export default defineConfig({ './react/vite.config.ts', './solid/vite.config.ts', './vue/vite.config.ts', + './scenarios/*/react/vite.config.ts', + './scenarios/*/solid/vite.config.ts', + './scenarios/*/vue/vite.config.ts', ], }, }) diff --git a/benchmarks/client-nav/vitest.react.config.ts b/benchmarks/client-nav/vitest.react.config.ts new file mode 100644 index 0000000000..a397ad929e --- /dev/null +++ b/benchmarks/client-nav/vitest.react.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./react/vite.config.ts', './scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/benchmarks/client-nav/vitest.solid.config.ts b/benchmarks/client-nav/vitest.solid.config.ts new file mode 100644 index 0000000000..b8827b86aa --- /dev/null +++ b/benchmarks/client-nav/vitest.solid.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./solid/vite.config.ts', './scenarios/*/solid/vite.config.ts'], + }, +}) diff --git a/benchmarks/client-nav/vitest.vue.config.ts b/benchmarks/client-nav/vitest.vue.config.ts new file mode 100644 index 0000000000..17960d7569 --- /dev/null +++ b/benchmarks/client-nav/vitest.vue.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./vue/vite.config.ts', './scenarios/*/vue/vite.config.ts'], + }, +}) diff --git a/benchmarks/client-nav/vue/vite.config.ts b/benchmarks/client-nav/vue/vite.config.ts index d917c82e2d..4c44c09fdd 100644 --- a/benchmarks/client-nav/vue/vite.config.ts +++ b/benchmarks/client-nav/vue/vite.config.ts @@ -1,9 +1,15 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -27,6 +33,6 @@ export default defineConfig({ name: '@benchmarks/client-nav (vue)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a7c3a7348..301a728be6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -285,6 +285,9 @@ importers: '@platformatic/flame': specifier: ^1.6.0 version: 1.6.0 + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../packages/router-plugin '@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) From b554a33266aaae6ef9b65b951dab7020dca4b0c8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:26:28 +0000 Subject: [PATCH 2/7] ci: apply automated fixes --- benchmarks/client-nav/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/client-nav/README.md b/benchmarks/client-nav/README.md index 3da0e70f5e..c9aa41514b 100644 --- a/benchmarks/client-nav/README.md +++ b/benchmarks/client-nav/README.md @@ -53,7 +53,7 @@ be attributed to a specific feature area. | Scenario | Client-side responsibility | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `react/`, `solid/`, `vue/` baseline apps | Mixed navigation loop: path params, search params, route context, and `useParams`/`useSearch`/`useLoaderData` selector subscriptions. | -| `async-pipeline` | The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async `beforeLoad` context, and parallel nested async loaders. Component-level `Await`/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark. | +| `async-pipeline` | The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async `beforeLoad` context, and parallel nested async loaders. Component-level `Await`/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark. | | `control-flow` | Loader-thrown `redirect` (including a 2-hop chain), `notFound()` with `notFoundComponent`, loader errors with `errorComponent`, and boundary reset on recovery navigation. | | `head` | `HeadContent` per-navigation work: nested route `head()` evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation. | | `history` | History push/replace/back/forward traversal, location masking, registered-but-never-blocking `useBlocker`, and `useCanGoBack`/`useLocation` subscriptions. | From 913cfd062d648f9e9cd877e26991f6bae69c7442 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 2 Jul 2026 16:48:27 +0200 Subject: [PATCH 3/7] chore(benchmarks): prefix client-nav bench names with "client-" All benches in @benchmarks/client-nav now start with "client" so they are easy to identify on the CodSpeed dashboard (the baseline already did). Co-Authored-By: Claude Fable 5 --- .../client-nav/scenarios/async-pipeline/react/speed.bench.ts | 2 +- .../client-nav/scenarios/async-pipeline/solid/speed.bench.ts | 2 +- .../client-nav/scenarios/async-pipeline/vue/speed.bench.ts | 2 +- .../client-nav/scenarios/control-flow/react/speed.bench.ts | 2 +- .../client-nav/scenarios/control-flow/solid/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/head/react/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/head/solid/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/head/vue/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/history/react/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/history/solid/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/history/vue/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/links/react/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/links/solid/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/links/vue/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/mount/react/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts | 2 +- .../client-nav/scenarios/nested-params/react/speed.bench.ts | 2 +- .../client-nav/scenarios/nested-params/solid/speed.bench.ts | 2 +- .../client-nav/scenarios/nested-params/vue/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/preload/react/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts | 2 +- benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts | 2 +- .../client-nav/scenarios/route-tree-scale/react/speed.bench.ts | 2 +- .../client-nav/scenarios/route-tree-scale/solid/speed.bench.ts | 2 +- .../client-nav/scenarios/route-tree-scale/vue/speed.bench.ts | 2 +- .../client-nav/scenarios/search-params/react/speed.bench.ts | 2 +- .../client-nav/scenarios/search-params/solid/speed.bench.ts | 2 +- .../client-nav/scenarios/search-params/vue/speed.bench.ts | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts index 20423f5663..68c5f97c2f 100644 --- a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-async-pipeline', () => { afterAll(test.after) bench( - 'async-pipeline navigation loop (react)', + 'client-async-pipeline navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts index ec134a6e33..d1f620f447 100644 --- a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-async-pipeline', () => { afterAll(test.after) bench( - 'async-pipeline navigation loop (solid)', + 'client-async-pipeline navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts index cd789259d2..eeb568323c 100644 --- a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-async-pipeline', () => { afterAll(test.after) bench( - 'async-pipeline navigation loop (vue)', + 'client-async-pipeline navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts index d76218944a..55a2224d3a 100644 --- a/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-control-flow', () => { afterAll(test.after) bench( - 'control-flow navigation loop (react)', + 'client-control-flow navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts index f3cad06d97..abbb85a575 100644 --- a/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-control-flow', () => { afterAll(test.after) bench( - 'control-flow navigation loop (solid)', + 'client-control-flow navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts index 0224291130..551b88e68f 100644 --- a/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-control-flow', () => { afterAll(test.after) bench( - 'control-flow navigation loop (vue)', + 'client-control-flow navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/head/react/speed.bench.ts b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts index 1c4c84d09d..c71b426c56 100644 --- a/benchmarks/client-nav/scenarios/head/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-head', () => { afterAll(test.after) bench( - 'head navigation loop (react)', + 'client-head navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts index 0f6be675c5..ea0a5ca818 100644 --- a/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-head', () => { afterAll(test.after) bench( - 'head navigation loop (solid)', + 'client-head navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts index 15f9e07143..3630032e44 100644 --- a/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-head', () => { afterAll(test.after) bench( - 'head navigation loop (vue)', + 'client-head navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/history/react/speed.bench.ts b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts index d84f17ebdb..df2c951c03 100644 --- a/benchmarks/client-nav/scenarios/history/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-history', () => { afterAll(test.after) bench( - 'history navigation loop (react)', + 'client-history navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts index 833b140e6a..8e814bc133 100644 --- a/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-history', () => { afterAll(test.after) bench( - 'history navigation loop (solid)', + 'client-history navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts index 98777d7ecd..be2956bd85 100644 --- a/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-history', () => { afterAll(test.after) bench( - 'history navigation loop (vue)', + 'client-history navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/links/react/speed.bench.ts b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts index 86343d5884..68bb869182 100644 --- a/benchmarks/client-nav/scenarios/links/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-links', () => { afterAll(test.after) bench( - 'links navigation loop (react)', + 'client-links navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts index 0f95d3d3f6..beba86fb6c 100644 --- a/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-links', () => { afterAll(test.after) bench( - 'links navigation loop (solid)', + 'client-links navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts index 4303aa91ef..54b99d1273 100644 --- a/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-links', () => { afterAll(test.after) bench( - 'links navigation loop (vue)', + 'client-links navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts index 073169fc45..7c44f1a0f7 100644 --- a/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-loaders', () => { afterAll(test.after) bench( - 'loaders navigation loop (react)', + 'client-loaders navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts index 5b9e158bfb..4c97823d11 100644 --- a/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-loaders', () => { afterAll(test.after) bench( - 'loaders navigation loop (solid)', + 'client-loaders navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts index f015fe908b..eeb9cefaeb 100644 --- a/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-loaders', () => { afterAll(test.after) bench( - 'loaders navigation loop (vue)', + 'client-loaders navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts index 864e931ed6..8f7967c42c 100644 --- a/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-mount', () => { afterAll(test.after) bench( - 'mount loop (react)', + 'client-mount loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts index a35b95c8bc..1bce4c6750 100644 --- a/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-mount', () => { afterAll(test.after) bench( - 'mount loop (solid)', + 'client-mount loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts index 01409d947b..d2e4b72c84 100644 --- a/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-mount', () => { afterAll(test.after) bench( - 'mount loop (vue)', + 'client-mount loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts index cb563390fa..321b1ad230 100644 --- a/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-nested-params', () => { afterAll(test.after) bench( - 'nested-params navigation loop (react)', + 'client-nested-params navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts index 1acf92206c..d35980f612 100644 --- a/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-nested-params', () => { afterAll(test.after) bench( - 'nested-params navigation loop (solid)', + 'client-nested-params navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts index 9cea79c687..fac8703c34 100644 --- a/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-nested-params', () => { afterAll(test.after) bench( - 'nested-params navigation loop (vue)', + 'client-nested-params navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts index 0f239245dc..c869ebdcfb 100644 --- a/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-preload', () => { afterAll(test.after) bench( - 'preload interaction loop (react)', + 'client-preload interaction loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts index 065cc92a39..ff2771985b 100644 --- a/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-preload', () => { afterAll(test.after) bench( - 'preload interaction loop (solid)', + 'client-preload interaction loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts index 66d4120133..2e790e9240 100644 --- a/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-preload', () => { afterAll(test.after) bench( - 'preload interaction loop (vue)', + 'client-preload interaction loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts index 47d42c8942..50cc26d5d6 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-route-tree-scale', () => { afterAll(test.after) bench( - 'route-tree-scale navigation loop (react)', + 'client-route-tree-scale navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts index 20bedc14e1..3d7a22a027 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-route-tree-scale', () => { afterAll(test.after) bench( - 'route-tree-scale navigation loop (solid)', + 'client-route-tree-scale navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts index 2ff2d5c8b4..a767b8d220 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-route-tree-scale', () => { afterAll(test.after) bench( - 'route-tree-scale navigation loop (vue)', + 'client-route-tree-scale navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts index 9f836fd495..c534611bd0 100644 --- a/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-search-params', () => { afterAll(test.after) bench( - 'search-params navigation loop (react)', + 'client-search-params navigation loop (react)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts index 9c21ef7a29..df6a24dc15 100644 --- a/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-search-params', () => { afterAll(test.after) bench( - 'search-params navigation loop (solid)', + 'client-search-params navigation loop (solid)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() diff --git a/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts index 2fcf07bc8c..0b394bbe27 100644 --- a/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts +++ b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts @@ -19,7 +19,7 @@ describe('client-search-params', () => { afterAll(test.after) bench( - 'search-params navigation loop (vue)', + 'client-search-params navigation loop (vue)', async () => { for (let i = 0; i < ticksPerIteration; i++) { await test.tick() From d0a25c71119a38e25a90991bb93e5d0a54000305 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 2 Jul 2026 17:27:51 +0200 Subject: [PATCH 4/7] fix(benchmarks): address review findings on client-nav scenario stability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes from an adversarial review of the scenario suite: - mount: create the browser history explicitly and destroy() it on unmount. The default per-router createBrowserHistory() monkey-patches window.history.pushState/replaceState, chaining one wrapper per router — per-mount cost degraded 1.8ms -> 6ms over 3000 mounts (O(N^2) drift in the cold-start bench). Verified flat (~1.86ms) after the fix. - links: replace the five components with setup-scoped useMatchRoute probes in all three apps. vue-router's MatchRoute calls useMatchRoute() inside its render function, leaking one undisposed watcher per render — the Vue bench accumulated ~5 dead subscribers per navigation. Verified flat per-tick cost over 2000 ticks after the fix. - all scenarios: bound the scroll-restoration cache with getScrollRestorationKey: (location) => location.pathname. The default key is a fresh random per-entry location key, so the module-level cache grew one entry per push navigation for the whole run. - harness hover step: dispatch a single mouseover instead of mouseover+mouseenter. Solid/Vue attach intent-preload handlers to BOTH events, so the old double dispatch ran the preload pipeline twice for them but once for React. Verified all three adapters preload exactly once per hover via cached-matches probes. - README: correct the loaders row (the invalidate step means cached routes re-run once per lap), the links row, and document the session-history growth characteristics. Co-Authored-By: Claude Fable 5 --- benchmarks/client-nav/README.md | 7 +++-- .../async-pipeline/react/src/main.tsx | 5 +++ .../async-pipeline/solid/src/main.tsx | 5 +++ .../scenarios/async-pipeline/vue/src/main.tsx | 5 +++ .../scenarios/control-flow/react/src/main.tsx | 5 +++ .../scenarios/control-flow/solid/src/main.tsx | 5 +++ .../scenarios/control-flow/vue/src/main.tsx | 5 +++ benchmarks/client-nav/scenarios/harness.ts | 12 +++---- .../scenarios/head/react/src/main.tsx | 5 +++ .../scenarios/head/solid/src/main.tsx | 5 +++ .../scenarios/head/vue/src/main.tsx | 5 +++ .../scenarios/history/react/src/main.tsx | 5 +++ .../scenarios/history/solid/src/main.tsx | 5 +++ .../scenarios/history/vue/src/main.tsx | 5 +++ .../scenarios/links/react/src/main.tsx | 5 +++ .../links/react/src/routes/__root.tsx | 29 ++++++++--------- .../scenarios/links/solid/src/main.tsx | 5 +++ .../links/solid/src/routes/__root.tsx | 29 ++++++++--------- .../scenarios/links/vue/src/main.tsx | 5 +++ .../scenarios/links/vue/src/routes/__root.tsx | 30 +++++++++--------- .../scenarios/loaders/react/src/main.tsx | 5 +++ .../scenarios/loaders/solid/src/main.tsx | 5 +++ .../scenarios/loaders/vue/src/main.tsx | 5 +++ .../scenarios/mount/react/src/main.tsx | 25 ++++++++++++--- .../scenarios/mount/solid/src/main.tsx | 31 +++++++++++++++---- .../scenarios/mount/vue/src/main.tsx | 25 ++++++++++++--- .../nested-params/react/src/main.tsx | 5 +++ .../nested-params/solid/src/main.tsx | 5 +++ .../scenarios/nested-params/vue/src/main.tsx | 5 +++ .../scenarios/preload/react/src/main.tsx | 5 +++ .../client-nav/scenarios/preload/shared.ts | 10 +++--- .../scenarios/preload/solid/src/main.tsx | 5 +++ .../scenarios/preload/vue/src/main.tsx | 5 +++ .../route-tree-scale/react/src/main.tsx | 5 +++ .../route-tree-scale/solid/src/main.tsx | 5 +++ .../route-tree-scale/vue/src/main.tsx | 5 +++ .../search-params/react/src/main.tsx | 5 +++ .../search-params/solid/src/main.tsx | 5 +++ .../scenarios/search-params/vue/src/main.tsx | 5 +++ 39 files changed, 277 insertions(+), 71 deletions(-) diff --git a/benchmarks/client-nav/README.md b/benchmarks/client-nav/README.md index c9aa41514b..bead1f59df 100644 --- a/benchmarks/client-nav/README.md +++ b/benchmarks/client-nav/README.md @@ -57,8 +57,8 @@ be attributed to a specific feature area. | `control-flow` | Loader-thrown `redirect` (including a 2-hop chain), `notFound()` with `notFoundComponent`, loader errors with `errorComponent`, and boundary reset on recovery navigation. | | `head` | `HeadContent` per-navigation work: nested route `head()` evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation. | | `history` | History push/replace/back/forward traversal, location masking, registered-but-never-blocking `useBlocker`, and `useCanGoBack`/`useLocation` subscriptions. | -| `links` | Per-navigation cost of ~200 mounted ``s: link prop building, active-state recompute across `activeOptions` variants, `activeProps` swaps, and `MatchRoute`/`useMatchRoute`. | -| `loaders` | Client loader dispatch: always-stale re-runs (`staleTime: 0`), pure cache hits, `loaderDeps`-keyed caching, `router.invalidate()`, and `useLoaderData` selectors. | +| `links` | Per-navigation cost of ~200 mounted ``s: link prop building, active-state recompute across `activeOptions` variants, `activeProps` swaps, and `useMatchRoute` probes (the `MatchRoute` component is avoided: vue-router's implementation leaks one subscription per render). | +| `loaders` | Client loader dispatch: always-stale re-runs (`staleTime: 0`), cached revisits (re-run once per lap by the `invalidate` step), `loaderDeps`-keyed caching, `router.invalidate()`, and `useLoaderData` selectors. | | `mount` | Cold start: `createRouter` (route-tree processing) + first render + initial `router.load()` + unmount, with a fresh router per mount. The only scenario measuring router creation. | | `nested-params` | Deep nesting (8 dynamic levels): per-level `params.parse`/`stringify`, `beforeLoad` context accumulation across matches, and per-level `useParams`/`useRouteContext` subscriptions. | | `preload` | Intent preloading from hover events, programmatic `router.preloadRoute`, deterministic preload cache behavior (`defaultPreloadStaleTime: 0`), and commit-time cache maintenance. | @@ -71,7 +71,8 @@ be attributed to a specific feature area. - Scenarios behave like a real user app: navigation happens through `` clicks dispatched on real anchor elements (unless a scenario specifically measures the imperative API), the router uses the default browser history, and `scrollRestoration` is enabled. - Each benchmark iteration advances a fixed, circular sequence of steps; every step awaits the router's `onRendered` event, so render work is included and steps cannot overlap. No two consecutive steps may target the same location, and the sequence ends back on the initial route. - Setup runs one warm-up lap through the sequence and asserts each step's observable output (e.g. `document.title`), so a scenario that silently stops doing its work fails instead of reporting a fast time. -- Determinism: no wall-clock timers (async work is resolved promises or counted `setTimeout(0)` hops), `staleTime`/`gcTime` only ever `0` or effectively-infinite, no `Math.random`/`Date.now` in the measured loop. +- Determinism: no wall-clock timers (async work is resolved promises or counted `setTimeout(0)` hops), `staleTime`/`gcTime` only ever `0` or effectively-infinite, no `Math.random`/`Date.now` in the measured loop. Scroll restoration uses `getScrollRestorationKey: (location) => location.pathname` so its cache stays bounded (the default random per-entry key grows one cache entry per push navigation). The `mount` scenario opts out of `scrollRestoration` entirely and destroys its history on unmount — both register page-lifetime globals that would leak across its mount/unmount loop. +- Push-only step laps grow jsdom's session-history entry list over a run (jsdom never truncates entries behind the current index). Per-push cost stays O(1) so timings are unaffected; only the `history` scenario needs — and has — a lap that is depth-stationary. - Calibration: pick per-iteration step counts so a bench's `vitest bench` run stays roughly between 8 and 30 seconds (long enough to average out, short enough for CI). ## Run diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx index 554c41e02d..4b141859e6 100644 --- a/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultPendingMs: 0, defaultPendingMinMs: 0, }) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx index 61b9030943..92dcc74d0a 100644 --- a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultPendingMs: 0, defaultPendingMinMs: 0, }) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx index cfd9ab44da..d8e659fb09 100644 --- a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultPendingMs: 0, defaultPendingMinMs: 0, }) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx index d480d61ba1..9a0c3d3b50 100644 --- a/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx index 54609e9388..817045bd02 100644 --- a/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx index 55507fa1e1..c96d3f3a2e 100644 --- a/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/harness.ts b/benchmarks/client-nav/scenarios/harness.ts index 21590d0d55..bed86cd832 100644 --- a/benchmarks/client-nav/scenarios/harness.ts +++ b/benchmarks/client-nav/scenarios/harness.ts @@ -40,9 +40,12 @@ export type ScenarioStep = } | { /** - * Dispatches `mouseover` + `mouseenter` on the link (React synthesizes - * `mouseEnter` from `mouseover`; Solid and Vue listen to the native - * `mouseenter`), which triggers `preload: 'intent'`, then settles. + * Dispatches a single `mouseover` on the link, which triggers + * `preload: 'intent'` exactly once in every adapter: React synthesizes + * `mouseEnter` from `mouseover`, and Solid/Vue attach a native + * `mouseover` preload handler. (Solid/Vue also listen to `mouseenter` — + * dispatching both events would run the preload pipeline twice for them + * but only once for React.) Then settles via counted 0ms hops. */ type: 'hover' testId: string @@ -162,9 +165,6 @@ export function createScenarioSetup(options: ScenarioSetupOptions) { link.dispatchEvent( new MouseEvent('mouseover', { bubbles: true, cancelable: true }), ) - link.dispatchEvent( - new MouseEvent('mouseenter', { bubbles: false, cancelable: false }), - ) await settle(step.settleHops ?? 4) return } diff --git a/benchmarks/client-nav/scenarios/head/react/src/main.tsx b/benchmarks/client-nav/scenarios/head/react/src/main.tsx index 51b72c7673..ef0b99555c 100644 --- a/benchmarks/client-nav/scenarios/head/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/head/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/head/solid/src/main.tsx b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx index 54609e9388..817045bd02 100644 --- a/benchmarks/client-nav/scenarios/head/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/head/vue/src/main.tsx b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx index 55507fa1e1..c96d3f3a2e 100644 --- a/benchmarks/client-nav/scenarios/head/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/history/react/src/main.tsx b/benchmarks/client-nav/scenarios/history/react/src/main.tsx index 51b72c7673..ef0b99555c 100644 --- a/benchmarks/client-nav/scenarios/history/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/history/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/history/solid/src/main.tsx b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx index 54609e9388..817045bd02 100644 --- a/benchmarks/client-nav/scenarios/history/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/history/vue/src/main.tsx b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx index 55507fa1e1..c96d3f3a2e 100644 --- a/benchmarks/client-nav/scenarios/history/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/links/react/src/main.tsx b/benchmarks/client-nav/scenarios/links/react/src/main.tsx index 51b72c7673..ef0b99555c 100644 --- a/benchmarks/client-nav/scenarios/links/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/links/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx index 48588a9ca4..7a48510e8a 100644 --- a/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx @@ -1,6 +1,5 @@ import { Link, - MatchRoute, Outlet, createRootRoute, useMatchRoute, @@ -26,19 +25,21 @@ function StatusPanel() { {matchRoute({ to: '/items/$id', params: { id } }) ? `on-${id}` : ''} ))} - {(match) => (match ? 'at-home' : '')} - - {(match) => (match ? 'at-about' : '')} - - - {(match) => (match ? 'under-root' : '')} - - - {(match) => (match ? 'under-about' : '')} - - - {(match) => (match ? 'about-search' : '')} - + {/* Static probes use the setup-scoped matchRoute instead of + : vue-router's MatchRoute creates an undisposed watcher + per render (one leaked subscription per navigation), so all three + apps use useMatchRoute for cross-framework parity. */} + {matchRoute({ to: '/' }) ? 'at-home' : ''} + {matchRoute({ to: '/about' }) ? 'at-about' : ''} + {matchRoute({ to: '/', fuzzy: true }) ? 'under-root' : ''} + + {matchRoute({ to: '/about', fuzzy: true }) ? 'under-about' : ''} + + + {matchRoute({ to: '/about', includeSearch: true }) + ? 'about-search' + : ''} + ) } diff --git a/benchmarks/client-nav/scenarios/links/solid/src/main.tsx b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx index 54609e9388..817045bd02 100644 --- a/benchmarks/client-nav/scenarios/links/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx index 1c2aac2945..a889a26854 100644 --- a/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx @@ -1,7 +1,6 @@ import { For } from 'solid-js' import { Link, - MatchRoute, Outlet, createRootRoute, useMatchRoute, @@ -31,19 +30,21 @@ function StatusPanel() { )} - {(match) => (match ? 'at-home' : '')} - - {(match) => (match ? 'at-about' : '')} - - - {(match) => (match ? 'under-root' : '')} - - - {(match) => (match ? 'under-about' : '')} - - - {(match) => (match ? 'about-search' : '')} - + {/* Static probes use the setup-scoped matchRoute instead of + : vue-router's MatchRoute creates an undisposed watcher + per render (one leaked subscription per navigation), so all three + apps use useMatchRoute for cross-framework parity. */} + {matchRoute({ to: '/' })() ? 'at-home' : ''} + {matchRoute({ to: '/about' })() ? 'at-about' : ''} + {matchRoute({ to: '/', fuzzy: true })() ? 'under-root' : ''} + + {matchRoute({ to: '/about', fuzzy: true })() ? 'under-about' : ''} + + + {matchRoute({ to: '/about', includeSearch: true })() + ? 'about-search' + : ''} + ) } diff --git a/benchmarks/client-nav/scenarios/links/vue/src/main.tsx b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx index 55507fa1e1..c96d3f3a2e 100644 --- a/benchmarks/client-nav/scenarios/links/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx index 61e34667fc..04f99677fd 100644 --- a/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx @@ -1,7 +1,6 @@ import * as Vue from 'vue' import { Link, - MatchRoute, Outlet, createRootRoute, useMatchRoute, @@ -20,27 +19,26 @@ const StatusPanel = Vue.defineComponent({ id, match: matchRoute({ to: '/items/$id', params: { id } }), })) + // Static probes use the setup-scoped matchRoute instead of : + // vue-router's MatchRoute creates an undisposed watcher per render (one + // leaked subscription per navigation), so all three apps use + // useMatchRoute for cross-framework parity. + const atHome = matchRoute({ to: '/' }) + const atAbout = matchRoute({ to: '/about' }) + const underRoot = matchRoute({ to: '/', fuzzy: true }) + const underAbout = matchRoute({ to: '/about', fuzzy: true }) + const aboutSearch = matchRoute({ to: '/about', includeSearch: true }) return () => ( ) }, diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx index 51b72c7673..ef0b99555c 100644 --- a/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx index 54609e9388..817045bd02 100644 --- a/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx index 55507fa1e1..c96d3f3a2e 100644 --- a/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/mount/react/src/main.tsx b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx index 51b72c7673..78fe2082aa 100644 --- a/benchmarks/client-nav/scenarios/mount/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx @@ -1,11 +1,26 @@ -import { RouterProvider, createRouter } from '@tanstack/react-router' +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/react-router' import { createRoot } from 'react-dom/client' import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/react-router' -export function createTestRouter() { +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { return createRouter({ routeTree, - scrollRestoration: true, + history, }) } @@ -16,7 +31,8 @@ declare module '@tanstack/react-router' { } export function mountTestApp(container: HTMLElement) { - const router = createTestRouter() + const history = createBrowserHistory() + const router = createTestRouter(history) const reactRoot = createRoot(container) reactRoot.render() @@ -25,6 +41,7 @@ export function mountTestApp(container: HTMLElement) { router, unmount() { reactRoot.unmount() + history.destroy() }, } } diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx index 54609e9388..ae03b145ca 100644 --- a/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx @@ -1,11 +1,26 @@ import { render } from 'solid-js/web' -import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/solid-router' import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/solid-router' -export function createTestRouter() { +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { return createRouter({ routeTree, - scrollRestoration: true, + history, }) } @@ -16,12 +31,16 @@ declare module '@tanstack/solid-router' { } export function mountTestApp(container: HTMLElement) { - const router = createTestRouter() + const history = createBrowserHistory() + const router = createTestRouter(history) - const unmount = render(() => , container) + const dispose = render(() => , container) return { router, - unmount, + unmount() { + dispose() + history.destroy() + }, } } diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx index 55507fa1e1..6d238e2832 100644 --- a/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx @@ -1,11 +1,26 @@ import * as Vue from 'vue' -import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/vue-router' import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/vue-router' -export function createTestRouter() { +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { return createRouter({ routeTree, - scrollRestoration: true, + history, }) } @@ -16,7 +31,8 @@ declare module '@tanstack/vue-router' { } export function mountTestApp(container: HTMLElement) { - const router = createTestRouter() + const history = createBrowserHistory() + const router = createTestRouter(history) const component = const app = Vue.createApp({ @@ -29,6 +45,7 @@ export function mountTestApp(container: HTMLElement) { router, unmount() { app.unmount() + history.destroy() }, } } diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx index 51b72c7673..ef0b99555c 100644 --- a/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx index 54609e9388..817045bd02 100644 --- a/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx index 55507fa1e1..c96d3f3a2e 100644 --- a/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/preload/react/src/main.tsx b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx index f81532b3a1..6f8942c118 100644 --- a/benchmarks/client-nav/scenarios/preload/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultPreload: 'intent', defaultPreloadDelay: 0, defaultPreloadStaleTime: 0, diff --git a/benchmarks/client-nav/scenarios/preload/shared.ts b/benchmarks/client-nav/scenarios/preload/shared.ts index 020f8aeea6..d061f99451 100644 --- a/benchmarks/client-nav/scenarios/preload/shared.ts +++ b/benchmarks/client-nav/scenarios/preload/shared.ts @@ -8,10 +8,12 @@ * work, keeping the loop stationary. * * Hover-intent preloading was verified manually per adapter (standalone jsdom - * run inspecting `router.state.cachedMatches` after dispatching - * `mouseover` + `mouseenter` on a link): React listens to the synthetic - * mouseEnter (driven by `mouseover`), Solid and Vue listen to the native - * `mouseenter` — the harness hover step dispatches both. + * run inspecting the router's cached matches after dispatching `mouseover` on + * a link). The harness hover step dispatches a single `mouseover`, which + * triggers the preload pipeline exactly once in every adapter: React + * synthesizes mouseEnter from it, Solid/Vue attach a native `mouseover` + * handler. (Solid/Vue also listen to `mouseenter`; dispatching both events + * would preload twice for them but once for React.) */ import type { ScenarioStep } from '../harness' diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx index c62ae4a8be..72e414c0a0 100644 --- a/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultPreload: 'intent', defaultPreloadDelay: 0, defaultPreloadStaleTime: 0, diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx index df1e70a83a..1e325392a4 100644 --- a/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultPreload: 'intent', defaultPreloadDelay: 0, defaultPreloadStaleTime: 0, diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx index 51b72c7673..ef0b99555c 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx index 54609e9388..817045bd02 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx index 55507fa1e1..c96d3f3a2e 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, }) } diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx index 0891f819d8..a68ef7bbb2 100644 --- a/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx +++ b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultStructuralSharing: true, }) } diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx index 8e3bf837f5..e95846e12d 100644 --- a/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultStructuralSharing: true, }) } diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx index 667228a6c2..1454b8d547 100644 --- a/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx @@ -6,6 +6,11 @@ export function createTestRouter() { return createRouter({ routeTree, scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, defaultStructuralSharing: true, }) } From f3adacc21fd17f208eae781bfc4f3edbf5bbb1d5 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 2 Jul 2026 18:00:57 +0200 Subject: [PATCH 5/7] feat(benchmarks): cover client rewrites, lazy route chunks, encoded params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the three coverage gaps identified in the scenario-suite analysis: - new `rewrites` scenario (react/solid/vue): router basepath '/app' composed with a locale input/output rewrite pair — the client analog of the SSR rewrites scenario. Every href build runs the output rewrite and every committed location runs the input rewrite; assertions check both the internal router pathname and the external window pathname. The harness gained an `initialUrl` option since a basepath app cannot start at '/'. - route-tree-scale now builds with `autoCodeSplitting: true`, so its navigations also exercise lazy route-chunk resolution — the default shape of real file-based apps, previously uncovered. - nested-params and route-tree-scale param sets now include characters that need percent-encoding (spaces, `&`, `%`, `+`, unicode) so the segment encode/decode paths run on every navigation. Co-Authored-By: Claude Fable 5 --- benchmarks/client-nav/README.md | 29 ++--- benchmarks/client-nav/package.json | 6 + benchmarks/client-nav/scenarios/harness.ts | 8 +- .../scenarios/nested-params/shared.ts | 13 ++- .../scenarios/rewrites/react/project.json | 40 +++++++ .../scenarios/rewrites/react/setup.ts | 18 +++ .../scenarios/rewrites/react/speed.bench.ts | 34 ++++++ .../scenarios/rewrites/react/speed.flame.ts | 18 +++ .../scenarios/rewrites/react/src/main.tsx | 38 +++++++ .../rewrites/react/src/routeTree.gen.ts | 103 ++++++++++++++++++ .../rewrites/react/src/routes/__root.tsx | 61 +++++++++++ .../rewrites/react/src/routes/index.tsx | 13 +++ .../rewrites/react/src/routes/p.$a.$b.tsx | 16 +++ .../rewrites/react/src/routes/p.$a.tsx | 17 +++ .../scenarios/rewrites/react/tsconfig.json | 21 ++++ .../scenarios/rewrites/react/vite.config.ts | 40 +++++++ .../client-nav/scenarios/rewrites/shared.ts | 101 +++++++++++++++++ .../scenarios/rewrites/solid/project.json | 40 +++++++ .../scenarios/rewrites/solid/setup.ts | 18 +++ .../scenarios/rewrites/solid/speed.bench.ts | 34 ++++++ .../scenarios/rewrites/solid/speed.flame.ts | 18 +++ .../scenarios/rewrites/solid/src/main.tsx | 35 ++++++ .../rewrites/solid/src/routeTree.gen.ts | 103 ++++++++++++++++++ .../rewrites/solid/src/routes/__root.tsx | 61 +++++++++++ .../rewrites/solid/src/routes/index.tsx | 13 +++ .../rewrites/solid/src/routes/p.$a.$b.tsx | 16 +++ .../rewrites/solid/src/routes/p.$a.tsx | 17 +++ .../scenarios/rewrites/solid/tsconfig.json | 21 ++++ .../scenarios/rewrites/solid/vite.config.ts | 48 ++++++++ .../scenarios/rewrites/vue/project.json | 40 +++++++ .../scenarios/rewrites/vue/setup.ts | 18 +++ .../scenarios/rewrites/vue/speed.bench.ts | 34 ++++++ .../scenarios/rewrites/vue/speed.flame.ts | 18 +++ .../scenarios/rewrites/vue/src/main.tsx | 42 +++++++ .../rewrites/vue/src/routeTree.gen.ts | 103 ++++++++++++++++++ .../rewrites/vue/src/routes/__root.tsx | 64 +++++++++++ .../rewrites/vue/src/routes/index.tsx | 16 +++ .../rewrites/vue/src/routes/p.$a.$b.tsx | 19 ++++ .../rewrites/vue/src/routes/p.$a.tsx | 20 ++++ .../scenarios/rewrites/vue/tsconfig.json | 21 ++++ .../scenarios/rewrites/vue/vite.config.ts | 42 +++++++ .../react/src/routes/__root.tsx | 18 ++- .../route-tree-scale/react/vite.config.ts | 3 + .../scenarios/route-tree-scale/shared.ts | 9 +- .../solid/src/routes/__root.tsx | 18 ++- .../route-tree-scale/solid/vite.config.ts | 3 + .../vue/src/routes/__root.tsx | 14 ++- .../route-tree-scale/vue/vite.config.ts | 3 + 48 files changed, 1473 insertions(+), 32 deletions(-) create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/project.json create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/setup.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/shared.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/project.json create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/setup.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/project.json create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/setup.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json create mode 100644 benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts diff --git a/benchmarks/client-nav/README.md b/benchmarks/client-nav/README.md index bead1f59df..6179c7f0b8 100644 --- a/benchmarks/client-nav/README.md +++ b/benchmarks/client-nav/README.md @@ -50,20 +50,21 @@ stable for CodSpeed continuity. Each scenario isolates one client-side responsibility so benchmark changes can be attributed to a specific feature area. -| Scenario | Client-side responsibility | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `react/`, `solid/`, `vue/` baseline apps | Mixed navigation loop: path params, search params, route context, and `useParams`/`useSearch`/`useLoaderData` selector subscriptions. | -| `async-pipeline` | The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async `beforeLoad` context, and parallel nested async loaders. Component-level `Await`/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark. | -| `control-flow` | Loader-thrown `redirect` (including a 2-hop chain), `notFound()` with `notFoundComponent`, loader errors with `errorComponent`, and boundary reset on recovery navigation. | -| `head` | `HeadContent` per-navigation work: nested route `head()` evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation. | -| `history` | History push/replace/back/forward traversal, location masking, registered-but-never-blocking `useBlocker`, and `useCanGoBack`/`useLocation` subscriptions. | -| `links` | Per-navigation cost of ~200 mounted ``s: link prop building, active-state recompute across `activeOptions` variants, `activeProps` swaps, and `useMatchRoute` probes (the `MatchRoute` component is avoided: vue-router's implementation leaks one subscription per render). | -| `loaders` | Client loader dispatch: always-stale re-runs (`staleTime: 0`), cached revisits (re-run once per lap by the `invalidate` step), `loaderDeps`-keyed caching, `router.invalidate()`, and `useLoaderData` selectors. | -| `mount` | Cold start: `createRouter` (route-tree processing) + first render + initial `router.load()` + unmount, with a fresh router per mount. The only scenario measuring router creation. | -| `nested-params` | Deep nesting (8 dynamic levels): per-level `params.parse`/`stringify`, `beforeLoad` context accumulation across matches, and per-level `useParams`/`useRouteContext` subscriptions. | -| `preload` | Intent preloading from hover events, programmatic `router.preloadRoute`, deterministic preload cache behavior (`defaultPreloadStaleTime: 0`), and commit-time cache maintenance. | -| `route-tree-scale` | Route matching and link-target resolution on a wide (~40 route) tree mixing static, dynamic, prefixed-param, splat, pathless-layout, and route-group paths. | -| `search-params` | `validateSearch` execution, search middlewares (`retainSearchParams`/`stripSearchParams`), functional search updaters, structural sharing, and `useSearch` selector subscriptions. | +| Scenario | Client-side responsibility | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `react/`, `solid/`, `vue/` baseline apps | Mixed navigation loop: path params, search params, route context, and `useParams`/`useSearch`/`useLoaderData` selector subscriptions. | +| `async-pipeline` | The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async `beforeLoad` context, and parallel nested async loaders. Component-level `Await`/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark. | +| `control-flow` | Loader-thrown `redirect` (including a 2-hop chain), `notFound()` with `notFoundComponent`, loader errors with `errorComponent`, and boundary reset on recovery navigation. | +| `head` | `HeadContent` per-navigation work: nested route `head()` evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation. | +| `history` | History push/replace/back/forward traversal, location masking, registered-but-never-blocking `useBlocker`, and `useCanGoBack`/`useLocation` subscriptions. | +| `links` | Per-navigation cost of ~200 mounted ``s: link prop building, active-state recompute across `activeOptions` variants, `activeProps` swaps, and `useMatchRoute` probes (the `MatchRoute` component is avoided: vue-router's implementation leaks one subscription per render). | +| `loaders` | Client loader dispatch: always-stale re-runs (`staleTime: 0`), cached revisits (re-run once per lap by the `invalidate` step), `loaderDeps`-keyed caching, `router.invalidate()`, and `useLoaderData` selectors. | +| `mount` | Cold start: `createRouter` (route-tree processing) + first render + initial `router.load()` + unmount, with a fresh router per mount. The only scenario measuring router creation. | +| `nested-params` | Deep nesting (8 dynamic levels): per-level `params.parse`/`stringify`, `beforeLoad` context accumulation across matches, and per-level `useParams`/`useRouteContext` subscriptions. Param values include characters requiring percent-encoding (as do `route-tree-scale`'s), so segment encode/decode paths run on every navigation. | +| `preload` | Intent preloading from hover events, programmatic `router.preloadRoute`, deterministic preload cache behavior (`defaultPreloadStaleTime: 0`), and commit-time cache maintenance. | +| `rewrites` | Composed client-side location rewrites: router `basepath` plus a locale input/output rewrite pair, running on every href build and location parse (the client analog of the SSR `rewrites` scenario). | +| `route-tree-scale` | Route matching and link-target resolution on a wide (~40 route) tree mixing static, dynamic, prefixed-param, splat, pathless-layout, and route-group paths, with `autoCodeSplitting` enabled so navigations also resolve lazy route chunks. | +| `search-params` | `validateSearch` execution, search middlewares (`retainSearchParams`/`stripSearchParams`), functional search updaters, structural sharing, and `useSearch` selector subscriptions. | ## Conventions diff --git a/benchmarks/client-nav/package.json b/benchmarks/client-nav/package.json index 73ad3ef5aa..701e068b49 100644 --- a/benchmarks/client-nav/package.json +++ b/benchmarks/client-nav/package.json @@ -64,6 +64,7 @@ "@benchmarks/client-nav-mount-react", "@benchmarks/client-nav-nested-params-react", "@benchmarks/client-nav-preload-react", + "@benchmarks/client-nav-rewrites-react", "@benchmarks/client-nav-route-tree-scale-react", "@benchmarks/client-nav-search-params-react" ], @@ -91,6 +92,7 @@ "@benchmarks/client-nav-mount-solid", "@benchmarks/client-nav-nested-params-solid", "@benchmarks/client-nav-preload-solid", + "@benchmarks/client-nav-rewrites-solid", "@benchmarks/client-nav-route-tree-scale-solid", "@benchmarks/client-nav-search-params-solid" ], @@ -118,6 +120,7 @@ "@benchmarks/client-nav-mount-vue", "@benchmarks/client-nav-nested-params-vue", "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-rewrites-vue", "@benchmarks/client-nav-route-tree-scale-vue", "@benchmarks/client-nav-search-params-vue" ], @@ -202,8 +205,11 @@ "@benchmarks/client-nav-preload-react", "@benchmarks/client-nav-preload-solid", "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-rewrites-react", "@benchmarks/client-nav-route-tree-scale-react", + "@benchmarks/client-nav-rewrites-solid", "@benchmarks/client-nav-route-tree-scale-solid", + "@benchmarks/client-nav-rewrites-vue", "@benchmarks/client-nav-route-tree-scale-vue", "@benchmarks/client-nav-search-params-react", "@benchmarks/client-nav-search-params-solid", diff --git a/benchmarks/client-nav/scenarios/harness.ts b/benchmarks/client-nav/scenarios/harness.ts index bed86cd832..7fff76d2e3 100644 --- a/benchmarks/client-nav/scenarios/harness.ts +++ b/benchmarks/client-nav/scenarios/harness.ts @@ -94,6 +94,12 @@ export interface ScenarioSetupOptions { steps: ReadonlyArray /** Sanity check run once per step during the warm-up lap in `before()`. */ assertAfterStep?: (stepIndex: number, container: HTMLElement) => void + /** + * URL the window is reset to before mounting (default '/'). Scenarios whose + * router uses a `basepath` or an input rewrite must start on an external + * URL that maps to their initial route. + */ + initialUrl?: string } function warnAboutDevMode(frameworkLabel: string) { @@ -121,7 +127,7 @@ export function createScenarioSetup(options: ScenarioSetupOptions) { async function before() { stepIndex = 0 - window.history.replaceState(null, '', '/') + window.history.replaceState(null, '', options.initialUrl ?? '/') container = document.createElement('div') document.body.append(container) diff --git a/benchmarks/client-nav/scenarios/nested-params/shared.ts b/benchmarks/client-nav/scenarios/nested-params/shared.ts index be43a3a6e7..06c968bb18 100644 --- a/benchmarks/client-nav/scenarios/nested-params/shared.ts +++ b/benchmarks/client-nav/scenarios/nested-params/shared.ts @@ -17,15 +17,18 @@ export interface LeafParams { h: string } +// Several values need percent-encoding in the URL (spaces, `&`, `%`, `+`, +// unicode) so every navigation exercises the param encode/decode paths, not +// just plain-ASCII interpolation. const baseParams: LeafParams = { a: 'alpha', - b: 'bravo', - c: 'charlie', + b: 'bravo & co', + c: 'charlie 100%', d: 'delta', - e: 'echo', - f: 'foxtrot', + e: 'écho-café', + f: 'foxtrot+one', g: 'golf', - h: 'hotel', + h: 'hôtel', } /** diff --git a/benchmarks/client-nav/scenarios/rewrites/react/project.json b/benchmarks/client-nav/scenarios/rewrites/react/project.json new file mode 100644 index 0000000000..d98c6e1412 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "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", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/setup.ts b/benchmarks/client-nav/scenarios/rewrites/react/setup.ts new file mode 100644 index 0000000000..0c5df273ed --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts new file mode 100644 index 0000000000..9b77a79838 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx new file mode 100644 index 0000000000..47746bb0fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx @@ -0,0 +1,38 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..b6ab8c8be4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/p/$a' | '/p/$a/$b' + id: '__root__' | '/' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx new file mode 100644 index 0000000000..ad14ff51f1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx new file mode 100644 index 0000000000..821cfe4a7c --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..ec1163e5c2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { itemLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemLabel(params.a, params.b)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx new file mode 100644 index 0000000000..f4937494f0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx @@ -0,0 +1,17 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { sectionLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + + return ( +
+

{sectionLabel(params.a)}

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts new file mode 100644 index 0000000000..8c07cff4fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav rewrites (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/shared.ts b/benchmarks/client-nav/scenarios/rewrites/shared.ts new file mode 100644 index 0000000000..c45b15d503 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/shared.ts @@ -0,0 +1,101 @@ +/** + * Shared definition of the `rewrites` scenario: composed client-side location + * rewrites — router `basepath` plus a locale input/output rewrite (the client + * analog of the SSR `rewrites` scenario). Externally URLs look like + * `/app/{locale}/p/...`; internally the router sees `/p/...` with the locale + * carried in the `_locale` search param. Every navigation runs the composed + * output rewrite when building hrefs/committing and the input rewrite when + * parsing the committed location. + */ +import type { ScenarioStep } from '../harness' + +export const basepath = '/app' +export const defaultLocale = 'en' + +const localePrefixRe = /^\/(fr|de|es)(\/.*)$/ + +// Framework-agnostic rewrite pair consumed by all three apps' createRouter. +export const localeRewrite = { + input: ({ url }: { url: URL }) => { + const match = url.pathname.match(localePrefixRe) + + if (match) { + url.pathname = match[2]! + url.searchParams.set('_locale', match[1]!) + return url + } + + return undefined + }, + output: ({ url }: { url: URL }) => { + const locale = url.searchParams.get('_locale') + + if (locale) { + url.searchParams.delete('_locale') + url.pathname = `/${locale}${url.pathname}` + } + + return url + }, +} + +export function sectionLabel(a: string) { + return `Section ${a}` +} + +export function itemLabel(a: string, b: string) { + return `Item ${a}/${b}` +} + +interface StepDef { + testId: string + /** Internal router pathname expected after the step. */ + routerPath: string + /** External window.location pathname expected after the step. */ + windowPath: string +} + +// Circular lap: alternates sections, depths, and locales so every step runs +// the composed rewrites in both directions, then returns to the start. +export const stepDefs: ReadonlyArray = [ + { testId: 'go-x', routerPath: '/p/x', windowPath: '/app/p/x' }, + { testId: 'go-x-1-fr', routerPath: '/p/x/1', windowPath: '/app/fr/p/x/1' }, + { testId: 'go-y-de', routerPath: '/p/y', windowPath: '/app/de/p/y' }, + { testId: 'go-y-2', routerPath: '/p/y/2', windowPath: '/app/p/y/2' }, + { testId: 'go-x-es', routerPath: '/p/x', windowPath: '/app/es/p/x' }, + { testId: 'go-home', routerPath: '/', windowPath: '/app/' }, +] as const + +export const steps: ReadonlyArray = stepDefs.map( + (step) => step.testId, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = stepDefs[stepIndex]! + const marker = container.querySelector('[data-testid="loc"]') + const routerPath = marker?.textContent + + if (routerPath !== expected.routerPath) { + throw new Error( + `Expected router pathname "${expected.routerPath}" after step ${stepIndex}, received "${routerPath}"`, + ) + } + + if (window.location.pathname !== expected.windowPath) { + throw new Error( + `Expected window pathname "${expected.windowPath}" after step ${stepIndex}, received "${window.location.pathname}"`, + ) + } +} + +// The app starts on the external root inside the basepath. +export const initialUrl = '/app' + +// Three laps through the 6-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/project.json b/benchmarks/client-nav/scenarios/rewrites/solid/project.json new file mode 100644 index 0000000000..4b2f763032 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts b/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts new file mode 100644 index 0000000000..5796df712d --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts new file mode 100644 index 0000000000..30d3436071 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx new file mode 100644 index 0000000000..a80de580fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx @@ -0,0 +1,35 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..2d35f899c9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/p/$a' | '/p/$a/$b' + id: '__root__' | '/' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..3a78f2c3cf --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx new file mode 100644 index 0000000000..5ee381ff05 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..febcad35e3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { itemLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemLabel(params().a, params().b)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx new file mode 100644 index 0000000000..adfb7bc3b6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx @@ -0,0 +1,17 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { sectionLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + + return ( +
+

{sectionLabel(params().a)}

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts new file mode 100644 index 0000000000..41613c2ad4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav rewrites (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/project.json b/benchmarks/client-nav/scenarios/rewrites/vue/project.json new file mode 100644 index 0000000000..9b7cb5d198 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts b/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts new file mode 100644 index 0000000000..e26b4d05ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts new file mode 100644 index 0000000000..c10b8723bb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx new file mode 100644 index 0000000000..83e430038d --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx @@ -0,0 +1,42 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..6eca4e8ce5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/p/$a' | '/p/$a/$b' + id: '__root__' | '/' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..ae89645005 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx @@ -0,0 +1,64 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx new file mode 100644 index 0000000000..d5405b52d2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Home

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..062c0903b9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { itemLabel } from '../../../shared' + +const ItemPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{itemLabel(params.value.a, params.value.b)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx new file mode 100644 index 0000000000..552700d402 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx @@ -0,0 +1,20 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { sectionLabel } from '../../../shared' + +const SectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{sectionLabel(params.value.a)}

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts new file mode 100644 index 0000000000..39e22b6c3e --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +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(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav rewrites (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx index 31df6ce3fb..7ed47da924 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx @@ -11,13 +11,21 @@ function RootComponent() { Home - + Sec A item Sec F settings - + Files Promo - + Sec C item diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts index feb768bfa7..686907a506 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts @@ -16,6 +16,9 @@ export default defineConfig({ codspeedPlugin(), tanstackRouter({ target: 'react', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, routesDirectory: `${rootDir}src/routes`, generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, }), diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts index 4e0ba89a0b..8af5996804 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts +++ b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts @@ -27,14 +27,17 @@ export const steps = [ 'go-home', ] as const +// The dynamic ids and the splat carry characters that need percent-encoding +// (spaces, `&`, `%`, `+`, unicode) so matching also exercises the segment +// encode/decode paths. const expectedMarkers = [ - 'sec-a:11', + 'sec-a:item 11%', 'sec-f:settings', - 'files:x/y/z', + 'files:x/ü y/z', 'release:9', 'alpha', 'promo', - 'sec-c:42', + 'sec-c:q&a+42', 'sec-d:about', 'home', ] as const diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx index 3695d0e216..a4e4113d9a 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx @@ -11,13 +11,21 @@ function RootComponent() { Home - + Sec A item Sec F settings - + Files Promo - + Sec C item diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts index 24ad9efde1..dd0bd234e0 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts @@ -16,6 +16,9 @@ export default defineConfig({ codspeedPlugin(), tanstackRouter({ target: 'solid', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, routesDirectory: `${rootDir}src/routes`, generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, }), diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx index 760e23d3e0..d2e1483106 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx @@ -9,7 +9,11 @@ const RootComponent = Vue.defineComponent({ Home - + Sec A item @@ -17,7 +21,7 @@ const RootComponent = Vue.defineComponent({ Files @@ -35,7 +39,11 @@ const RootComponent = Vue.defineComponent({ Promo - + Sec C item diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts index 5cd87f517d..a547406198 100644 --- a/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts @@ -17,6 +17,9 @@ export default defineConfig({ codspeedPlugin(), tanstackRouter({ target: 'vue', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, routesDirectory: `${rootDir}src/routes`, generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, }), From fdb8d2cb56c5b6f8c1175d1315e5e414cb57a4ac Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 2 Jul 2026 23:21:23 +0200 Subject: [PATCH 6/7] fix(benchmarks): reduce syscall time in measured loops, lengthen mount bench CodSpeed excludes syscall time from simulation measures, and past a threshold it warns ("cannot be consistently instrumented") and skips the benchmark; the exclusion is also inconsistent run-to-run, which was the root cause of the two same-code CI swings (route-tree-scale react 13%, async-pipeline react 3.6%) and of skipped runs on hop-heavy benches like async-pipeline solid (579 calls / 12.1ms). - Replace all counted setTimeout(0) hops with setImmediate hops (harness timerHop + async-pipeline hopDelay). Both are deterministic event-loop turns that yield to timers and the React Node scheduler, but a timer costs ~3-4 syscalls (timerfd + epoll) per hop vs ~1 for an immediate. Same-machine instrumented before/after: preload solid 39.8ms -> 1.2ms of syscall time, preload vue 30.1 -> 1.1, async-pipeline solid 23.5 -> 1.1, async-pipeline vue 17.9 -> 0.9. head/route-tree-scale are unchanged (their residual syscalls are GC/scheduler, and their measures were already stable); history's syscalls come from jsdom's internal timer-queued traversal and are not reachable from bench code (its measured values are stable regardless). - mount: 6 ticks per iteration instead of 2. A single mount simulates to only ~8ms and very short measures amplify allocator/GC quantization; the bench now measures ~50ms+ of simulated CPU. All 39 benches pass; full nx typecheck green. Co-Authored-By: Claude Fable 5 --- .../client-nav/scenarios/async-pipeline/shared.ts | 10 ++++++++-- benchmarks/client-nav/scenarios/harness.ts | 11 ++++++++++- benchmarks/client-nav/scenarios/mount/shared.ts | 5 ++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/shared.ts b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts index c82f8c18ae..02123309b2 100644 --- a/benchmarks/client-nav/scenarios/async-pipeline/shared.ts +++ b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts @@ -14,12 +14,18 @@ */ import type { ScenarioStep } from '../harness' -/** Resolves after exactly `hops` chained 0ms timer hops. */ +/** + * Resolves after exactly `hops` chained macrotask turns. Uses `setImmediate` + * rather than `setTimeout(0)`: both are counted, deterministic event-loop + * turns, but timers cost ~3-4 syscalls per hop (timerfd + epoll) and CodSpeed + * excludes syscall time from the measure — inconsistently past a threshold — + * which destabilized the hop-heavy benches. + */ export function hopDelay(hops: number): Promise { let promise = Promise.resolve() for (let i = 0; i < hops; i++) { promise = promise.then( - () => new Promise((resolve) => setTimeout(resolve, 0)), + () => new Promise((resolve) => setImmediate(() => resolve())), ) } return promise diff --git a/benchmarks/client-nav/scenarios/harness.ts b/benchmarks/client-nav/scenarios/harness.ts index 7fff76d2e3..a71da3366d 100644 --- a/benchmarks/client-nav/scenarios/harness.ts +++ b/benchmarks/client-nav/scenarios/harness.ts @@ -58,7 +58,16 @@ export type ScenarioStep = | { type: 'forward' } | { type: 'go'; delta: number } -const timerHop = () => new Promise((resolve) => setTimeout(resolve, 0)) +/** + * One macrotask turn via `setImmediate` rather than `setTimeout(0)`: both + * yield deterministically to the event loop (timers, MessageChannel, React's + * Node scheduler), but a timer costs ~3-4 syscalls (timerfd + epoll) per hop + * while an immediate costs ~1. CodSpeed excludes syscall time from the + * measure — inconsistently past a threshold, which flagged the hop-heavy + * benches as "skipped" — so the loop must stay syscall-lean. + */ +const timerHop = () => + new Promise((resolve) => setImmediate(() => resolve())) async function settle(hops: number) { for (let i = 0; i < hops; i++) { diff --git a/benchmarks/client-nav/scenarios/mount/shared.ts b/benchmarks/client-nav/scenarios/mount/shared.ts index a0e6df2392..a8f3d2c7bd 100644 --- a/benchmarks/client-nav/scenarios/mount/shared.ts +++ b/benchmarks/client-nav/scenarios/mount/shared.ts @@ -63,7 +63,10 @@ export function assertReady(container: HTMLElement) { } // Mounts per benchmark iteration. -export const ticksPerIteration = 2 +// 6 mounts per iteration: a single mount simulates to only ~8ms of CPU, and +// very short measures amplify the relative impact of allocator/GC +// quantization; 6 keeps the measure comfortably above ~50ms simulated. +export const ticksPerIteration = 6 export const benchOptions = { warmupIterations: 100, From f3736913a7528473ee3fa7d14a916a724803d0e0 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Fri, 3 Jul 2026 00:38:34 +0200 Subject: [PATCH 7/7] fix(benchmarks): stop history benches sleeping in jsdom's traversal timers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The history benches were skip-warned by CodSpeed for excessive syscall time (5-26ms, highly variable across runs). Root cause: jsdom delivers history traversals through two nested window.setTimeout(0) tasks, and Node clamps zero timers to 1ms of wall time — so while awaiting onRendered after back/forward/go, the event loop blocked in epoll_wait until each timer expired. That blocked wall-time is recorded as syscall time, and its duration depends on host timer behavior, hence the variability. The harness now reroutes zero-delay window.setTimeout calls onto setImmediate for the duration of a benchmark (non-zero delays pass through; clearTimeout handles both). Zero-delay timeouts carry no ordering semantics a check-phase immediate doesn't satisfy, and the suite's conventions already forbid real-delay timers in measured code. Instrumented A/A verification (2 runs per framework): history syscall time react 23-26ms -> 2.5ms, solid 14-23ms -> 1.1ms, vue 11-12ms -> 1.2ms, with bit-identical syscall counts across runs and measured values stable to 0.06%. The benches also stopped measuring sleep: locally an iteration dropped from ~42-49ms to ~5-11ms of actual CPU work. All 39 benches pass. Co-Authored-By: Claude Fable 5 --- benchmarks/client-nav/scenarios/harness.ts | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/benchmarks/client-nav/scenarios/harness.ts b/benchmarks/client-nav/scenarios/harness.ts index a71da3366d..7b993d0ed8 100644 --- a/benchmarks/client-nav/scenarios/harness.ts +++ b/benchmarks/client-nav/scenarios/harness.ts @@ -89,6 +89,60 @@ async function settleUntil(isSettled: () => boolean, label: string) { ) } +/** + * Reroute zero-delay `window.setTimeout` calls onto `setImmediate` for the + * duration of the benchmark. jsdom delivers history traversals through two + * nested `window.setTimeout(0)` tasks; Node clamps those to 1ms of wall + * time, so with nothing else pending the event loop blocks in `epoll_wait` + * until each timer expires — and that blocked wall-time is recorded as + * highly variable syscall time (5-26ms across runs), enough for CodSpeed to + * skip-warn the history benches. Zero-delay timeouts carry no ordering + * semantics a check-phase immediate doesn't satisfy, and the suite's + * conventions already forbid real-delay timers in measured code; non-zero + * delays are passed through untouched. + */ +function patchZeroDelayTimeouts() { + const originalSetTimeout = window.setTimeout.bind(window) + const originalClearTimeout = window.clearTimeout.bind(window) + const immediates = new Map() + // Negative ids cannot collide with real jsdom timer ids. + let nextImmediateId = -2 + + window.setTimeout = (( + handler: (...handlerArgs: Array) => void, + delay?: number, + ...args: Array + ) => { + if (!delay) { + const id = nextImmediateId-- + immediates.set( + id, + setImmediate(() => { + immediates.delete(id) + handler(...args) + }), + ) + return id + } + return originalSetTimeout(handler, delay, ...args) + }) as typeof window.setTimeout + + window.clearTimeout = ((id: number) => { + const immediate = immediates.get(id) + if (immediate) { + clearImmediate(immediate) + immediates.delete(id) + return + } + originalClearTimeout(id) + }) as typeof window.clearTimeout + + return () => { + window.setTimeout = originalSetTimeout as typeof window.setTimeout + window.clearTimeout = originalClearTimeout as typeof window.clearTimeout + } +} + export interface ScenarioSetupOptions { frameworkLabel: string mount: (container: HTMLElement) => MountedScenarioApp @@ -130,12 +184,14 @@ export function createScenarioSetup(options: ScenarioSetupOptions) { let container: HTMLDivElement | undefined = undefined let unmount: (() => void) | undefined = undefined let unsub = () => {} + let restoreTimeouts = () => {} let stepIndex = 0 let next: () => Promise = () => Promise.reject(new Error('Benchmark not initialized')) async function before() { stepIndex = 0 + restoreTimeouts = patchZeroDelayTimeouts() window.history.replaceState(null, '', options.initialUrl ?? '/') container = document.createElement('div') document.body.append(container) @@ -252,6 +308,8 @@ export function createScenarioSetup(options: ScenarioSetupOptions) { unmount?.() container?.remove() unsub() + restoreTimeouts() + restoreTimeouts = () => {} unmount = undefined container = undefined }