Skip to content

Commit 3dcc016

Browse files
improve bundle size benchmarks and add initial skill (#7450)
1 parent fee0b58 commit 3dcc016

23 files changed

Lines changed: 1399 additions & 31 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../skills/bundle-size-optimization

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ TanStack Router is a type-safe router with built-in caching and URL state manage
2121
- Framework-agnostic core logic separated from React/Solid bindings
2222
- Type-safe routing with search params and path params
2323
- Use workspace protocol for internal dependencies (`workspace:*`)
24+
- Always use curly braces for `if`, `else`, loops, and similar control statements. Never write one-line bodies like `if (foo) x = 1`.
2425

2526
## Dev environment tips
2627

benchmarks/bundle-size/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,41 @@ Each package has `minimal` and `full` scenarios:
3232
pnpm nx run @benchmarks/bundle-size:build
3333
```
3434

35+
Run one or more scenarios during local optimization:
36+
37+
```bash
38+
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal
39+
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal,react-router.full
40+
```
41+
42+
Filtered runs build only the package projects needed by selected scenarios. Full runs build all package projects needed by all scenarios. If the required packages are already built and unchanged, skip that step:
43+
44+
```bash
45+
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal --skip-package-builds
46+
```
47+
3548
This writes:
3649

3750
- `benchmarks/bundle-size/results/current.json`
3851
- `benchmarks/bundle-size/results/benchmark-action.json`
3952

53+
`current.json` includes run status, selected package build projects, per-scenario totals, per-file sizes, and the emitted JS files used for measurement. Dist paths use `scenarioDir`/`outDir`, e.g. `react-router.minimal` maps to `benchmarks/bundle-size/dist/react-router-minimal/`.
54+
55+
## Local Query Tools
56+
57+
```bash
58+
pnpm benchmark:bundle-size:query --id react-router.minimal
59+
pnpm benchmark:bundle-size:diff --baseline /tmp/base-current.json --id react-router.minimal
60+
pnpm benchmark:bundle-size:history --id react-router.minimal --top-deltas 20
61+
```
62+
63+
For source attribution, run an analysis build. This uses hidden source maps and writes source estimates into `current.json`; those estimates are for investigation only, not tracking.
64+
65+
```bash
66+
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal --analysis
67+
pnpm benchmark:bundle-size:analyze --id react-router.minimal --top-sources 30
68+
```
69+
4070
## CI Reporting
4171

4272
- PR workflow generates a sticky comment with:
@@ -56,6 +86,10 @@ The measurement script supports optional interfaces for historical backfilling:
5686
- `--sha`
5787
- `--measured-at`
5888
- `--append-history`
89+
- `--scenario`
90+
- `--analysis`
91+
- `--sourcemap`
92+
- `--skip-package-builds`
5993

6094
These are intended for one-off scripts that replay historical commits and append results to the same history dataset shape used for chart generation.
6195
If `--append-history` points at a `data.js` file, output is written as `window.BENCHMARK_DATA = ...` for direct GitHub Pages compatibility.

benchmarks/bundle-size/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@
55
"scripts": {
66
"build": "node ../../scripts/benchmarks/bundle-size/measure.mjs"
77
},
8+
"nx": {
9+
"targets": {
10+
"build": {
11+
"cache": false,
12+
"dependsOn": []
13+
}
14+
}
15+
},
816
"dependencies": {
917
"@tanstack/react-router": "workspace:^",
1018
"@tanstack/solid-router": "workspace:^",
1119
"@tanstack/vue-router": "workspace:^",
1220
"@tanstack/react-start": "workspace:^",
1321
"@tanstack/solid-start": "workspace:^",
22+
"@tanstack/vue-start": "workspace:^",
1423
"react": "^19.0.0",
1524
"react-dom": "^19.0.0",
1625
"solid-js": "^1.9.10",

benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@ import { pluginReact } from '@rsbuild/plugin-react'
33
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'
44

55
const outDir = process.env.BUNDLE_SIZE_DIST_DIR ?? 'dist-rsbuild'
6+
const clientOutput = process.env.BUNDLE_SIZE_RSB_CLIENT_OUTPUT
7+
const startOptions = clientOutput
8+
? {
9+
rsbuild: {
10+
client: {
11+
output: clientOutput as 'module' | 'iife',
12+
},
13+
},
14+
}
15+
: undefined
616

717
export default defineConfig({
818
logLevel: 'silent',
9-
plugins: [pluginReact({ splitChunks: false }), tanstackStart()],
19+
plugins: [pluginReact({ splitChunks: false }), tanstackStart(startOptions)],
1020
output: {
1121
distPath: {
1222
root: outDir,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createRouter } from '@tanstack/vue-router'
2+
import { routeTree } from './routeTree.gen'
3+
4+
export function getRouter() {
5+
return createRouter({
6+
routeTree,
7+
scrollRestoration: true,
8+
})
9+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import {
2+
Asset,
3+
Await,
4+
Block,
5+
Body,
6+
CatchBoundary,
7+
CatchNotFound,
8+
ClientOnly,
9+
DefaultGlobalNotFound,
10+
ErrorComponent,
11+
HeadContent,
12+
Html,
13+
Link,
14+
Match,
15+
MatchRoute,
16+
Matches,
17+
Navigate,
18+
Outlet,
19+
RouterContextProvider,
20+
ScriptOnce,
21+
Scripts,
22+
ScrollRestoration,
23+
createLink,
24+
createRootRoute,
25+
linkOptions,
26+
useAwaited,
27+
useBlocker,
28+
useCanGoBack,
29+
useChildMatches,
30+
useElementScrollRestoration,
31+
useLinkProps,
32+
useLoaderData,
33+
useLoaderDeps,
34+
useLocation,
35+
useMatch,
36+
useMatchRoute,
37+
useMatches,
38+
useNavigate,
39+
useParams,
40+
useParentMatches,
41+
useRouteContext,
42+
useRouter,
43+
useRouterState,
44+
useSearch,
45+
useTags,
46+
} from '@tanstack/vue-router'
47+
import {
48+
createMiddleware,
49+
createServerFn,
50+
useServerFn,
51+
} from '@tanstack/vue-start'
52+
53+
const requestMiddleware = createMiddleware().server(async ({ next }) => {
54+
return next()
55+
})
56+
57+
const functionMiddleware = createMiddleware({ type: 'function' })
58+
.client(async ({ next }) => {
59+
return next()
60+
})
61+
.server(async ({ next }) => {
62+
return next()
63+
})
64+
65+
const helloServerFn = createServerFn({ method: 'GET' })
66+
.middleware([requestMiddleware, functionMiddleware])
67+
.handler(async () => {
68+
return 'hello from server fn'
69+
})
70+
71+
export const Route = createRootRoute({
72+
component: RootComponent,
73+
})
74+
75+
function RootComponent() {
76+
const router = useRouter()
77+
const [awaited] = useAwaited({ promise: Promise.resolve('ready') })
78+
const linkProps = useLinkProps({ to: '/' } as any)
79+
const matchRoute = useMatchRoute()
80+
const matches = useMatches()
81+
const parentMatches = useParentMatches()
82+
const childMatches = useChildMatches()
83+
const match = useMatch({ strict: false, shouldThrow: false } as any)
84+
const loaderDeps = useLoaderDeps({ strict: false } as any)
85+
const loaderData = useLoaderData({ strict: false } as any)
86+
const params = useParams({ strict: false } as any)
87+
const search = useSearch({ strict: false } as any)
88+
const routeContext = useRouteContext({ strict: false } as any)
89+
const routerState = useRouterState({ select: (state) => state.status } as any)
90+
const location = useLocation()
91+
const canGoBack = useCanGoBack()
92+
const navigate = useNavigate()
93+
const scrollEntry = useElementScrollRestoration({ id: 'root-scroll' })
94+
const tags = useTags()
95+
const invokeServerFn = useServerFn(helloServerFn)
96+
97+
useBlocker({
98+
shouldBlockFn: () => false,
99+
disabled: true,
100+
withResolver: false,
101+
})
102+
103+
const linkFactoryResult = linkOptions({ to: '/' } as any)
104+
const routeMatchResult = matchRoute({ to: '/' } as any)
105+
const SvgLink = createLink('svg')
106+
107+
const startSurface = [createMiddleware, createServerFn, useServerFn]
108+
const hooksAndComponents = [
109+
useAwaited,
110+
useLinkProps,
111+
useMatchRoute,
112+
useMatches,
113+
useParentMatches,
114+
useChildMatches,
115+
useMatch,
116+
useLoaderDeps,
117+
useLoaderData,
118+
useBlocker,
119+
useNavigate,
120+
useParams,
121+
useSearch,
122+
useRouteContext,
123+
useRouter,
124+
useRouterState,
125+
useLocation,
126+
useCanGoBack,
127+
useElementScrollRestoration,
128+
useTags,
129+
Await,
130+
CatchBoundary,
131+
CatchNotFound,
132+
ClientOnly,
133+
DefaultGlobalNotFound,
134+
ErrorComponent,
135+
Link,
136+
Match,
137+
MatchRoute,
138+
Matches,
139+
Navigate,
140+
Outlet,
141+
RouterContextProvider,
142+
ScrollRestoration,
143+
Block,
144+
ScriptOnce,
145+
Asset,
146+
HeadContent,
147+
Scripts,
148+
Body,
149+
Html,
150+
]
151+
152+
;(globalThis as any).__TANSTACK_BUNDLE_SIZE_KEEP__ = {
153+
hooksAndComponents,
154+
startSurface,
155+
}
156+
157+
void awaited
158+
void linkFactoryResult
159+
void matches.value
160+
void parentMatches.value
161+
void childMatches.value
162+
void match.value
163+
void loaderDeps.value
164+
void loaderData.value
165+
void params.value
166+
void search.value
167+
void routeContext.value
168+
void routerState.value
169+
void location.value
170+
void canGoBack.value
171+
void navigate
172+
void scrollEntry
173+
void tags()
174+
void routeMatchResult.value
175+
void invokeServerFn
176+
177+
return (
178+
<Html>
179+
<head>
180+
<HeadContent />
181+
</head>
182+
<Body>
183+
<ScriptOnce>{'window.__tsr_bundle_size = true'}</ScriptOnce>
184+
<Asset
185+
tag="meta"
186+
attrs={{ name: 'bundle-size', content: 'vue-start-full' }}
187+
/>
188+
<Link {...(linkProps as any)}>home</Link>
189+
<SvgLink to="/" aria-label="svg-home">
190+
<circle cx="8" cy="8" r="7" />
191+
</SvgLink>
192+
<MatchRoute to="/">{() => <span data-test="match-route" />}</MatchRoute>
193+
<ClientOnly fallback={<span data-test="client-only-fallback" />}>
194+
<span data-test="client-only" />
195+
</ClientOnly>
196+
<Await
197+
promise={Promise.resolve('done')}
198+
children={() => <span data-test="await" />}
199+
/>
200+
<Block shouldBlockFn={() => false} disabled withResolver={false}>
201+
{() => <span data-test="block" />}
202+
</Block>
203+
<CatchNotFound fallback={() => <DefaultGlobalNotFound />}>
204+
<span data-test="catch-not-found" />
205+
</CatchNotFound>
206+
<RouterContextProvider router={router}>
207+
<span data-test="nested-router-context" />
208+
</RouterContextProvider>
209+
<ScrollRestoration />
210+
<Outlet />
211+
<Scripts />
212+
<div data-test="full-root">
213+
<div>hello world</div>
214+
</div>
215+
</Body>
216+
</Html>
217+
)
218+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createFileRoute } from '@tanstack/vue-router'
2+
3+
export const Route = createFileRoute('/')({
4+
component: IndexComponent,
5+
})
6+
7+
function IndexComponent() {
8+
return <div>hello world</div>
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from 'vite'
2+
import vue from '@vitejs/plugin-vue'
3+
import vueJsx from '@vitejs/plugin-vue-jsx'
4+
import { tanstackStart } from '@tanstack/vue-start/plugin/vite'
5+
6+
export default defineConfig({
7+
plugins: [tanstackStart(), vue(), vueJsx()],
8+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createRouter } from '@tanstack/vue-router'
2+
import { routeTree } from './routeTree.gen'
3+
4+
export function getRouter() {
5+
return createRouter({
6+
routeTree,
7+
scrollRestoration: true,
8+
})
9+
}

0 commit comments

Comments
 (0)