-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
improve bundle size benchmarks and add initial skill #7450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../skills/bundle-size-optimization |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,10 +3,20 @@ import { pluginReact } from '@rsbuild/plugin-react' | |
| import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild' | ||
|
|
||
| const outDir = process.env.BUNDLE_SIZE_DIST_DIR ?? 'dist-rsbuild' | ||
| const clientOutput = process.env.BUNDLE_SIZE_RSB_CLIENT_OUTPUT | ||
| const startOptions = clientOutput | ||
| ? { | ||
| rsbuild: { | ||
| client: { | ||
| output: clientOutput as 'module' | 'iife', | ||
| }, | ||
| }, | ||
| } | ||
| : undefined | ||
|
Comment on lines
+7
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: As of May 2026, there is no configuration property named rsbuild.client.output in either Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and print the relevant config file
FILE="benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts"
if [ -f "$FILE" ]; then
echo "=== $FILE ==="
nl -ba "$FILE" | sed -n '1,120p'
else
echo "File not found: $FILE"
ls -R benchmarks/bundle-size/scenarios/react-start-minimal | head
fi
echo
echo "=== Search for BUNDLE_SIZE_RSB_CLIENT_OUTPUT usage ==="
rg -n "BUNDLE_SIZE_RSB_CLIENT_OUTPUT" -S .
echo
echo "=== Search for 'tanstackStart(' usage in this config ==="
rg -n "tanstackStart\(" -S benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts
echo
echo "=== Search for 'output:' with module/iife strings in repo ==="
rg -n "output:\s*['\"](module|iife)['\"]" -S . || true
echo
echo "=== Search for 'client: {' and rsbuild: structure patterns ==="
rg -n "rsbuild:\s*{\s*client:\s*{|\bclient:\s*{\s*output" -S benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts || trueRepository: TanStack/router Length of output: 199 🌐 Web query:
💡 Result: Answer: In Rsbuid (Rspack-based), “client output module iife” doesn’t correspond to a single TanStack Start setting. What you actually control is Rsbuild output format: 1) Default: IIFE for web targets Rsbuild states that when building a web application, it generates output in the IIFE format by default [1]. 2) Switching away from IIFE to ES modules If you want ES module output instead of IIFE, Rsbuild provides output.module (boolean). Rsbuild says: “When building a web application, Rsbuild generates the output in the IIFE format by default. If you want to output ES Modules instead, set output.module to true.” [1]. Example: export default { output: { module: true, }, }; When output.module is enabled, Rsbuild also adds type="module" to generated script tags [1]. 3) Node target default differs For completeness: Rsbuild says Node.js apps output ES modules format by default, and you can change to CommonJS via output.module = false when output.target is node [1]. 4) How this relates to TanStack Start “rsbuild plugin” TanStack Start’s rsbuild integration is implemented as Rsbuild plugins (e.g., Start rsbuild plugin core plus framework-specific rsbuild plugin entries) [2][3]. However, the IIFE-vs-ESM detail is governed by Rsbuild’s output configuration (not an “iife” option specific to TanStack Start, based on the Rsbuild output.module documentation) [1]. So, to get “client module” output (ESM) rather than IIFE in a TanStack Start + Rsbuild web build, set Rsbuild output.module=true; otherwise you’ll get IIFE by default for web [1]. Citations:
🌐 Web query:
💡 Result: In Rsbuild, the default web-app JavaScript format is IIFE. If you want an ES-module output instead, set output.module = true; this results in ES modules and Rsbuild sets scriptLoading to module in the generated HTML. [1] So for the TanStack React Start rsbuild plugin: it doesn’t (by itself) switch you away from IIFE. To get module output (instead of IIFE), you configure Rsbuild’s output.module. [1] Example (rsbuild.config.ts): import { defineConfig } from ' Citations: 🌐 Web query:
💡 Result: In Rsbuild, “IIFE” output is controlled by building for the web target, not by an explicit “IIFE” flag. 1) When does Rsbuild emit IIFE? - For web applications (output.target = 'web', which is the default), Rsbuild generates output in the IIFE format by default. [1] 2) How to configure client output “module” (ESM) vs IIFE - Rsbuild has output.module (boolean). It is described as outputting JavaScript in ES modules format, and it is only available when output.target is 'web' or 'node'. [1] - If you want ES Modules instead of the default IIFE for web apps, set output.module = true. [1] Example (web/client => IIFE default): export default { output: { target: 'web', // output.module omitted/false => IIFE by default }, }; Example (web/client => ESM, not IIFE): export default { output: { target: 'web', module: true, }, }; 3) Node client (if you’re using environments for client/server) - For output.target = 'node', Rsbuild outputs ES modules by default, and you can set output.module = false to output CommonJS instead. [1] If you share your exact “rsbuild client” setup (e.g., whether you use environments and what you set for output.target), I can map it precisely to the correct config block. Citations: 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and print the relevant config file
FILE="benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts"
if [ -f "$FILE" ]; then
echo "=== $FILE ==="
nl -ba "$FILE" | sed -n '1,120p'
else
echo "File not found: $FILE"
ls -R benchmarks/bundle-size/scenarios/react-start-minimal | head
fi
echo
echo "=== Search for BUNDLE_SIZE_RSB_CLIENT_OUTPUT usage ==="
rg -n "BUNDLE_SIZE_RSB_CLIENT_OUTPUT" -S .
echo
echo "=== Search for 'tanstackStart(' usage in this config ==="
rg -n "tanstackStart\(" -S benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts
echo
echo "=== Search for 'output:' with module/iife strings in repo ==="
rg -n "output:\s*['\"](module|iife)['\"]" -S . || true
echo
echo "=== Search for 'client: {' and rsbuild: structure patterns ==="
rg -n "rsbuild:\s*{\s*client:\s*{|\bclient:\s*{\s*output" -S benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts || trueRepository: TanStack/router Length of output: 199 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts"
echo "=== FILE CONTENTS: $FILE ==="
if [ -f "$FILE" ]; then
sed -n '1,160p' "$FILE" | cat
else
echo "MISSING: $FILE"
exit 1
fi
echo
echo "=== SHOW PACKAGE JSON(S) FOR DEPENDENCY VERSIONS ==="
# Try common locations; if missing, still show what exists.
for p in \
"benchmarks/bundle-size/package.json" \
"benchmarks/package.json" \
"package.json" \
; do
if [ -f "$p" ]; then
echo "--- $p ---"
cat "$p"
fi
done
echo
echo "=== RIPGREP: look for tanstackStart and rsbuild.client.output usage ==="
rg -n "tanstackStart\\(" benchmarks/bundle-size/scenarios/react-start-minimal -S || true
rg -n "rsbuild:\\s*\\{[\\s\\S]*client[\\s\\S]*output" benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts -S || true
rg -n "client:\\s*\\{[\\s\\S]*output" benchmarks/bundle-size/scenarios/react-start-minimal/rsbuild.config.ts -S || trueRepository: TanStack/router Length of output: 7409 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find implementation/types for tanstackStart in the monorepo
rg -n "export (function|const|default) tanstackStart|tanstackStart\\s*\\(" packages -S || true
rg -n "tanstackStart" packages/@tanstack -S || true
# Look for rsbuild plugin option typing, especially rsbuild.client.output
rg -n "rsbuild\\s*:\\s*\\{" packages -S || true
rg -n "client\\s*:\\s*\\{[\\s\\S]*output" packages -S || true
rg -n "output\\s*:\\s*'module'\\s*\\|\\s*'iife'|\"module\"\\s*\\|\\s*\"iife\"" -S packages || true
rg -n "module'\\s*\\|\\s*'iife|iife'\\s*\\|" -S packages || true
# Locate files under `@tanstack/react-start/plugin/rsbuild`
fd -t f "rsbuild" packages | head -n 50
fd -t f "*react-start*rsbuild*" packages | head -n 50Repository: TanStack/router Length of output: 5395 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find implementation/types for tanstackStart in the monorepo
rg -n "export (function|const|default) tanstackStart|tanstackStart\\s*\\(" packages -S || true
rg -n "tanstackStart" packages/@tanstack -S || true
# Look for rsbuild plugin option typing, especially rsbuild.client.output
rg -n "rsbuild\\s*:\\s*\\{" packages -S || true
rg -n "client\\s*:\\s*\\{[\\s\\S]*output" packages -S || true
rg -n "output\\s*:\\s*'module'\\s*\\|\\s*'iife'|\"module\"\\s*\\|\\s*\"iife\"" -S packages || true
rg -n "module'\\s*\\|\\s*'iife|iife'\\s*\\|" -S packages || true
# Locate files under `@tanstack/react-start/plugin/rsbuild`
fd -t f "rsbuild" packages | head -n 50
fd -t f "*react-start*rsbuild*" packages | head -n 50Repository: TanStack/router Length of output: 5395 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Print relevant parts of TanStack Start rsbuild plugin implementation and schema
sed -n '1,220p' packages/react-start/src/plugin/rsbuild.ts | cat
echo "-----"
sed -n '1,220p' packages/start-plugin-core/src/rsbuild/schema.ts | cat
echo "-----"
sed -n '1,120p' packages/router-core/src/manifest.ts | catRepository: TanStack/router Length of output: 7144 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n "parseStartConfig\\(" packages/start-plugin-core -S
rg -n "tanstackStartRsbuildOptionsSchema\\.parse\\(" packages/start-plugin-core -S
rg -n "tanStackStartRsbuild\\(" packages/start-plugin-core -S
rg -n "tanStackStartRsbuild" packages/start-plugin-core -SRepository: TanStack/router Length of output: 684 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,220p' packages/start-plugin-core/src/rsbuild/plugin.ts | cat
echo "-----"
sed -n '1,120p' packages/start-plugin-core/src/rsbuild/index.ts | catRepository: TanStack/router Length of output: 9554 Validate Line 11 force-casts the env value to Proposed fix const outDir = process.env.BUNDLE_SIZE_DIST_DIR ?? 'dist-rsbuild'
const clientOutput = process.env.BUNDLE_SIZE_RSB_CLIENT_OUTPUT
+const isValidClientOutput =
+ clientOutput === 'module' || clientOutput === 'iife'
+
+if (clientOutput && !isValidClientOutput) {
+ throw new Error(
+ `Invalid BUNDLE_SIZE_RSB_CLIENT_OUTPUT: ${clientOutput}. Expected "module" or "iife".`,
+ )
+}
+
const startOptions = clientOutput
? {
rsbuild: {
client: {
- output: clientOutput as 'module' | 'iife',
+ output: clientOutput,
},
},
}
: undefined🤖 Prompt for AI Agents |
||
|
|
||
| export default defineConfig({ | ||
| logLevel: 'silent', | ||
| plugins: [pluginReact({ splitChunks: false }), tanstackStart()], | ||
| plugins: [pluginReact({ splitChunks: false }), tanstackStart(startOptions)], | ||
| output: { | ||
| distPath: { | ||
| root: outDir, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { createRouter } from '@tanstack/vue-router' | ||
| import { routeTree } from './routeTree.gen' | ||
|
|
||
| export function getRouter() { | ||
| return createRouter({ | ||
| routeTree, | ||
| scrollRestoration: true, | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| import { | ||
| Asset, | ||
| Await, | ||
| Block, | ||
| Body, | ||
| CatchBoundary, | ||
| CatchNotFound, | ||
| ClientOnly, | ||
| DefaultGlobalNotFound, | ||
| ErrorComponent, | ||
| HeadContent, | ||
| Html, | ||
| Link, | ||
| Match, | ||
| MatchRoute, | ||
| Matches, | ||
| Navigate, | ||
| Outlet, | ||
| RouterContextProvider, | ||
| ScriptOnce, | ||
| Scripts, | ||
| ScrollRestoration, | ||
| createLink, | ||
| createRootRoute, | ||
| linkOptions, | ||
| useAwaited, | ||
| useBlocker, | ||
| useCanGoBack, | ||
| useChildMatches, | ||
| useElementScrollRestoration, | ||
| useLinkProps, | ||
| useLoaderData, | ||
| useLoaderDeps, | ||
| useLocation, | ||
| useMatch, | ||
| useMatchRoute, | ||
| useMatches, | ||
| useNavigate, | ||
| useParams, | ||
| useParentMatches, | ||
| useRouteContext, | ||
| useRouter, | ||
| useRouterState, | ||
| useSearch, | ||
| useTags, | ||
| } from '@tanstack/vue-router' | ||
| import { | ||
| createMiddleware, | ||
| createServerFn, | ||
| useServerFn, | ||
| } from '@tanstack/vue-start' | ||
|
|
||
| const requestMiddleware = createMiddleware().server(async ({ next }) => { | ||
| return next() | ||
| }) | ||
|
|
||
| const functionMiddleware = createMiddleware({ type: 'function' }) | ||
| .client(async ({ next }) => { | ||
| return next() | ||
| }) | ||
| .server(async ({ next }) => { | ||
| return next() | ||
| }) | ||
|
|
||
| const helloServerFn = createServerFn({ method: 'GET' }) | ||
| .middleware([requestMiddleware, functionMiddleware]) | ||
| .handler(async () => { | ||
| return 'hello from server fn' | ||
| }) | ||
|
|
||
| export const Route = createRootRoute({ | ||
| component: RootComponent, | ||
| }) | ||
|
|
||
| function RootComponent() { | ||
| const router = useRouter() | ||
| const [awaited] = useAwaited({ promise: Promise.resolve('ready') }) | ||
| const linkProps = useLinkProps({ to: '/' } as any) | ||
| const matchRoute = useMatchRoute() | ||
| const matches = useMatches() | ||
| const parentMatches = useParentMatches() | ||
| const childMatches = useChildMatches() | ||
| const match = useMatch({ strict: false, shouldThrow: false } as any) | ||
| const loaderDeps = useLoaderDeps({ strict: false } as any) | ||
| const loaderData = useLoaderData({ strict: false } as any) | ||
| const params = useParams({ strict: false } as any) | ||
| const search = useSearch({ strict: false } as any) | ||
| const routeContext = useRouteContext({ strict: false } as any) | ||
| const routerState = useRouterState({ select: (state) => state.status } as any) | ||
|
Comment on lines
+78
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify strict TS settings and enumerate current `as any` usages in the changed benchmark surfaces.
set -euo pipefail
echo "== tsconfig files with strict options =="
fd -i 'tsconfig.*json' | xargs -I{} sh -c 'echo "--- {}"; rg -n "\"strict\"|\"noImplicitAny\"|\"strictNullChecks\"" "{}" || true'
echo
echo "== 'as any' usages in bundle-size scenarios =="
rg -n --type=ts --type=tsx '\bas any\b' benchmarks/bundle-size/scenariosRepository: TanStack/router Length of output: 25034 Avoid
🤖 Prompt for AI Agents |
||
| const location = useLocation() | ||
| const canGoBack = useCanGoBack() | ||
| const navigate = useNavigate() | ||
| const scrollEntry = useElementScrollRestoration({ id: 'root-scroll' }) | ||
| const tags = useTags() | ||
| const invokeServerFn = useServerFn(helloServerFn) | ||
|
|
||
| useBlocker({ | ||
| shouldBlockFn: () => false, | ||
| disabled: true, | ||
| withResolver: false, | ||
| }) | ||
|
|
||
| const linkFactoryResult = linkOptions({ to: '/' } as any) | ||
| const routeMatchResult = matchRoute({ to: '/' } as any) | ||
| const SvgLink = createLink('svg') | ||
|
|
||
| const startSurface = [createMiddleware, createServerFn, useServerFn] | ||
| const hooksAndComponents = [ | ||
| useAwaited, | ||
| useLinkProps, | ||
| useMatchRoute, | ||
| useMatches, | ||
| useParentMatches, | ||
| useChildMatches, | ||
| useMatch, | ||
| useLoaderDeps, | ||
| useLoaderData, | ||
| useBlocker, | ||
| useNavigate, | ||
| useParams, | ||
| useSearch, | ||
| useRouteContext, | ||
| useRouter, | ||
| useRouterState, | ||
| useLocation, | ||
| useCanGoBack, | ||
| useElementScrollRestoration, | ||
| useTags, | ||
| Await, | ||
| CatchBoundary, | ||
| CatchNotFound, | ||
| ClientOnly, | ||
| DefaultGlobalNotFound, | ||
| ErrorComponent, | ||
| Link, | ||
| Match, | ||
| MatchRoute, | ||
| Matches, | ||
| Navigate, | ||
| Outlet, | ||
| RouterContextProvider, | ||
| ScrollRestoration, | ||
| Block, | ||
| ScriptOnce, | ||
| Asset, | ||
| HeadContent, | ||
| Scripts, | ||
| Body, | ||
| Html, | ||
| ] | ||
|
|
||
| ;(globalThis as any).__TANSTACK_BUNDLE_SIZE_KEEP__ = { | ||
| hooksAndComponents, | ||
| startSurface, | ||
| } | ||
|
|
||
| void awaited | ||
| void linkFactoryResult | ||
| void matches.value | ||
| void parentMatches.value | ||
| void childMatches.value | ||
| void match.value | ||
| void loaderDeps.value | ||
| void loaderData.value | ||
| void params.value | ||
| void search.value | ||
| void routeContext.value | ||
| void routerState.value | ||
| void location.value | ||
| void canGoBack.value | ||
| void navigate | ||
| void scrollEntry | ||
| void tags() | ||
| void routeMatchResult.value | ||
| void invokeServerFn | ||
|
|
||
| return ( | ||
| <Html> | ||
| <head> | ||
| <HeadContent /> | ||
| </head> | ||
| <Body> | ||
| <ScriptOnce>{'window.__tsr_bundle_size = true'}</ScriptOnce> | ||
| <Asset | ||
| tag="meta" | ||
| attrs={{ name: 'bundle-size', content: 'vue-start-full' }} | ||
| /> | ||
| <Link {...(linkProps as any)}>home</Link> | ||
| <SvgLink to="/" aria-label="svg-home"> | ||
| <circle cx="8" cy="8" r="7" /> | ||
| </SvgLink> | ||
| <MatchRoute to="/">{() => <span data-test="match-route" />}</MatchRoute> | ||
| <ClientOnly fallback={<span data-test="client-only-fallback" />}> | ||
| <span data-test="client-only" /> | ||
| </ClientOnly> | ||
| <Await | ||
| promise={Promise.resolve('done')} | ||
| children={() => <span data-test="await" />} | ||
| /> | ||
| <Block shouldBlockFn={() => false} disabled withResolver={false}> | ||
| {() => <span data-test="block" />} | ||
| </Block> | ||
| <CatchNotFound fallback={() => <DefaultGlobalNotFound />}> | ||
| <span data-test="catch-not-found" /> | ||
| </CatchNotFound> | ||
| <RouterContextProvider router={router}> | ||
| <span data-test="nested-router-context" /> | ||
| </RouterContextProvider> | ||
| <ScrollRestoration /> | ||
| <Outlet /> | ||
| <Scripts /> | ||
| <div data-test="full-root"> | ||
| <div>hello world</div> | ||
| </div> | ||
| </Body> | ||
| </Html> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { createFileRoute } from '@tanstack/vue-router' | ||
|
|
||
| export const Route = createFileRoute('/')({ | ||
| component: IndexComponent, | ||
| }) | ||
|
|
||
| function IndexComponent() { | ||
| return <div>hello world</div> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { defineConfig } from 'vite' | ||
| import vue from '@vitejs/plugin-vue' | ||
| import vueJsx from '@vitejs/plugin-vue-jsx' | ||
| import { tanstackStart } from '@tanstack/vue-start/plugin/vite' | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [tanstackStart(), vue(), vueJsx()], | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { createRouter } from '@tanstack/vue-router' | ||
| import { routeTree } from './routeTree.gen' | ||
|
|
||
| export function getRouter() { | ||
| return createRouter({ | ||
| routeTree, | ||
| scrollRestoration: true, | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { | ||
| Body, | ||
| HeadContent, | ||
| Html, | ||
| Outlet, | ||
| Scripts, | ||
| createRootRoute, | ||
| } from '@tanstack/vue-router' | ||
|
|
||
| export const Route = createRootRoute({ | ||
| component: RootComponent, | ||
| }) | ||
|
|
||
| function RootComponent() { | ||
| return ( | ||
| <Html> | ||
| <head> | ||
| <HeadContent /> | ||
| </head> | ||
| <Body> | ||
| <Outlet /> | ||
| <Scripts /> | ||
| </Body> | ||
| </Html> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
workspace:*for the newly added internal dependency.Line 22 uses
workspace:^, but repo rules requireworkspace:*for internal packages.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents