diff --git a/scripts/size/.npmrc b/scripts/size/.npmrc
new file mode 100644
index 000000000..99c103d40
--- /dev/null
+++ b/scripts/size/.npmrc
@@ -0,0 +1,4 @@
+# @solidjs/router declares solid-js / @solidjs/web as peers; the scenarios
+# alias both to the built dists, so the peers must not be installed here
+# (they would pull a published solid-js into this lockfile for nothing).
+legacy-peer-deps=true
diff --git a/scripts/size/README.md b/scripts/size/README.md
index 453d329ee..432f3b418 100644
--- a/scripts/size/README.md
+++ b/scripts/size/README.md
@@ -7,8 +7,8 @@ primitives — that keeps the store engine pay-for-use under `hydrate()`, three
compiled-template scenarios — a compiled floor and a JSX todo app in CSR and
hydrating form — the frames client as a package, two server-component PAGES:
base and live, their two compiled counterparts — the same pages as JSX with
-the templates a real page has — and two server-entry floors:
-`getRequestEvent`/`isServer` and
+the templates a real page has — the same two hand-written pages under
+`@solidjs/router`, and two server-entry floors: `getRequestEvent`/`isServer` and
`renderToString`) with hard brotli limits on the eager entry chunk. CI fails
when a scenario exceeds its limit and grew more than a small minified
allowance over its base (see [The gate](#the-gate)) — that means tree-shaking regressed, or a deliberate
@@ -123,6 +123,19 @@ switch re-based every cap (Rolldown lands 4–9% lower on the same artifacts;
the table is in the switch PR and each scenario's ledger note in
`scenarios.js`). Ledger deltas must not be read across that line.
+## The router scenarios
+
+`page: base + router` and `page: live + router` are the two page scenarios
+plus `@solidjs/router` 2.0 — the Solid 2 line (`2.0.0-next.*`), a
+devDependency of this directory pinned exactly like Rolldown (a router
+upgrade is a re-base, recorded like any other cap change). It resolves from
+this directory's `node_modules`; its `solid-js` / `@solidjs/web` imports go
+through the page alias to the built dists like everything else, and
+`.npmrc`'s `legacy-peer-deps` keeps npm from installing those peers from the
+registry for nothing. The router's own modules report as the `other`
+package. These are not floors: their caps are inline, and the two page
+floors stay the gate on the runtime alone.
+
## Compiled scenarios
The hand-written `app:` fixtures call the runtime directly and never compile
diff --git a/scripts/size/package-lock.json b/scripts/size/package-lock.json
index 0d462dc9b..b4d36befb 100644
--- a/scripts/size/package-lock.json
+++ b/scripts/size/package-lock.json
@@ -6,6 +6,7 @@
"": {
"name": "@solidjs/size-scenarios",
"devDependencies": {
+ "@solidjs/router": "2.0.0-next.35",
"rolldown": "1.2.11"
}
},
@@ -299,6 +300,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@solidjs/router": {
+ "version": "2.0.0-next.35",
+ "resolved": "https://registry.npmjs.org/@solidjs/router/-/router-2.0.0-next.35.tgz",
+ "integrity": "sha512-6+frAz9M2vrLSz+2hr51++nzGyxt3+nCo08dY3PkdieSOwDutAXdfSXj681FqTOvffv8kPDF5tdRqPUoLbKhlQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@solidjs/web": "^2.0.0-rc.13",
+ "filesystem-routing": ">=0.4.0",
+ "solid-js": "^2.0.0-rc.13"
+ },
+ "peerDependenciesMeta": {
+ "filesystem-routing": {
+ "optional": true
+ }
+ }
+ },
"node_modules/rolldown": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz",
diff --git a/scripts/size/package.json b/scripts/size/package.json
index 6997aba6a..c268bf45d 100644
--- a/scripts/size/package.json
+++ b/scripts/size/package.json
@@ -12,6 +12,7 @@
"build": "cd ../.. && pnpm install --frozen-lockfile && pnpm build"
},
"devDependencies": {
+ "@solidjs/router": "2.0.0-next.35",
"rolldown": "1.2.11"
}
}
diff --git a/scripts/size/sc-live-router-app.js b/scripts/size/sc-live-router-app.js
new file mode 100644
index 000000000..4b6178ba9
--- /dev/null
+++ b/scripts/size/sc-live-router-app.js
@@ -0,0 +1,49 @@
+// The live server-component page under a router: sc-live-app plus the same
+// `@solidjs/router` 2.0 composition as sc-router-app (createRouter with two
+// routes — one `preload`, one lazy — the instance as the hydrated root,
+// `useNavigate` in a route component). See sc-router-app.js for why the
+// router's runtime, not just its imports, is retained.
+import {
+ hydrate,
+ Show,
+ For,
+ Loading,
+ Errored,
+ dynamicComponent,
+ createComponent
+} from "@solidjs/web";
+import { createSignal, createMemo, action, isPending, latest, lazy } from "solid-js";
+import { installServerComponents } from "@solidjs/web/frames";
+import { createServerReference, live, GET } from "@solidjs/web/server-functions/client";
+import { createRouter, useNavigate } from "@solidjs/router";
+
+installServerComponents();
+const getStory = live(GET(createServerReference("story", "getStory")));
+const Story = dynamicComponent(() => getStory());
+const send = action(async () => {});
+const [n, setN] = createSignal(0);
+const Page = lazy(() => import("./lazy-page.js"));
+function Home(props) {
+ const navigate = useNavigate();
+ return [props.children, () => navigate("/story/1")];
+}
+const Router = createRouter({
+ routes: [
+ { path: "/", component: Home, preload: ({ params, intent }) => void (params, intent) },
+ { path: "/story/:id", component: Page }
+ ]
+});
+hydrate(() => {
+ const d = createMemo(() => n() + (isPending(n) ? 1 : 0) + latest(n));
+ setN(1);
+ return [
+ d(),
+ Show,
+ For,
+ Loading,
+ Errored,
+ Story,
+ send,
+ createComponent(Router, { children: p => p.children })
+ ];
+}, document.body);
diff --git a/scripts/size/sc-router-app.js b/scripts/size/sc-router-app.js
new file mode 100644
index 000000000..c03176757
--- /dev/null
+++ b/scripts/size/sc-router-app.js
@@ -0,0 +1,56 @@
+// The base server-component page under a router: sc-base-app plus what an
+// application page ships to route — `@solidjs/router` 2.0 (the Solid 2 line,
+// `2.0.0-next.*`, installed by this directory's own package.json; its
+// `solid-js` / `@solidjs/web` imports route to the dists through pageAlias
+// like every other import here). `createRouter` with two routes — one with a
+// `preload`, one whose component is the page's `lazy()` chunk — rendered as
+// the hydrated root through the instance with a render-prop layout, and
+// `useNavigate` read in a route component. 2.0 has no ``: anchors are
+// plain ``, claimed by the router through `registerElementClaim` and
+// intercepted by delegation, so the link runtime is the router's own
+// (`setupLinkClaims`, `setupNativeEvents`) and is retained by the instance.
+// The instance, its routes and the route components are all reachable from
+// the root, so the router's runtime — matching, the integration, the
+// context, preloading, link claims — is retained, not just its imports.
+import {
+ hydrate,
+ Show,
+ For,
+ Loading,
+ Errored,
+ dynamicComponent,
+ createComponent
+} from "@solidjs/web";
+import { createSignal, createMemo, lazy } from "solid-js";
+import { installServerComponents } from "@solidjs/web/frames";
+import { createServerReference } from "@solidjs/web/server-functions/client";
+import { createRouter, useNavigate } from "@solidjs/router";
+
+installServerComponents();
+const getStory = createServerReference("story", "getStory");
+const Story = dynamicComponent(() => getStory());
+const [n, setN] = createSignal(0);
+const Page = lazy(() => import("./lazy-page.js"));
+function Home(props) {
+ const navigate = useNavigate();
+ return [props.children, () => navigate("/story/1")];
+}
+const Router = createRouter({
+ routes: [
+ { path: "/", component: Home, preload: ({ params, intent }) => void (params, intent) },
+ { path: "/story/:id", component: Page }
+ ]
+});
+hydrate(() => {
+ const d = createMemo(() => n() + 1);
+ setN(1);
+ return [
+ d(),
+ Show,
+ For,
+ Loading,
+ Errored,
+ Story,
+ createComponent(Router, { children: p => p.children })
+ ];
+}, document.body);
diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js
index a47a3600e..05da999b3 100644
--- a/scripts/size/scenarios.js
+++ b/scripts/size/scenarios.js
@@ -4305,6 +4305,67 @@ module.exports = [
capMinified: 122779,
alias: pageAlias
},
+ {
+ name: "page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate)",
+ // The base page as an application ships it: with a router. The two page
+ // floors above carry no router, so the "base case" they gate is a page
+ // nobody deploys; this scenario is the same composition plus
+ // `@solidjs/router` 2.0 (`2.0.0-next.35`, pinned exactly in this
+ // directory's package.json — the Solid 2 line; a router upgrade is a
+ // re-base recorded like a Rolldown upgrade) with `createRouter`, two
+ // routes (one `preload`, one lazy), the instance as the hydrated root
+ // and `useNavigate` in a route component. Not a floor: an inline cap,
+ // and the router's growth is the router's — this scenario attributes it
+ // (the `other` package in the report is the router).
+ // Landing (2026-10-06, next @ 49a8dca84): measured locally at 57,001 B
+ // (184,378 B minified) against the base page's 44,864 (145,599) — the
+ // router's contribution +12,137 B br / +38,779 B minified: the router's
+ // own 28,008 minified (`createRouterContext`, `setupNativeEvents` incl.
+ // the form-action path, `query`'s preload cache, `setupLinkClaims`,
+ // scroll restoration, `browserHistory`) plus what it retains of ours —
+ // signals +8,583 (`action` and the optimistic lanes: `verdictValue`,
+ // `dissolveLane`, `laneRead`, `isPending`/`latest`, `createEffect`,
+ // `onSettled`), sf +1,188 (`subscribeFlightData`, `decodeResponsePayload`,
+ // the redirect header, `parseServerFunctionAddress`), web +1,137
+ // (`takeHydrationValue`, `registerElementClaim`), solid +364
+ // (`createContext`, `children`). The router standalone, with solid-js /
+ // @solidjs/web external, is 32,430 / 11,216 B.
+ // Re-based onto `next` @ 7233451ee (#3838, 2026-10-07), after the frames
+ // tiers (#3860): measured at 45,940 B (144,082 B minified) against the
+ // base page's 33,908 (105,252) — the router's contribution +12,032 B br
+ // / +38,830 B minified, the same as at landing; the page under it is
+ // what #3860 cut. The fixture now mounts through `dynamicComponent`, the
+ // documented server-component mount the base page uses (#3870); with
+ // `dynamic` it measures 48,272 / 151,551 (+2,332 / +7,469: `dynamic`'s
+ // tag arm keeps the element runtime). The router does not retain that
+ // runtime itself — its `serverRouteComponent` helper mounts with
+ // `dynamic`, but this page does not use it. Cap at measured + 10 B
+ // rounded up to 0.01 KB; recorded minified 144,082 B (CI-confirmed).
+ path: "sc-router-app.js",
+ limit: "45.95 KB",
+ capMinified: 144082,
+ alias: pageAlias
+ },
+ {
+ name: "page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate)",
+ // The live page with the same router (see the base + router note).
+ // Landing (2026-10-06, next @ 49a8dca84): measured locally at 58,243 B
+ // (188,800 B minified) against the live page's 48,527 (157,562) — the
+ // router's contribution +9,716 B br / +31,238 B minified, less than on
+ // the base page because the live page already carries `action` and
+ // `isPending`/`latest`, which the router retains on every page.
+ // Re-based onto `next` @ 7233451ee (#3838, 2026-10-07), after the frames
+ // tiers (#3860): measured at 47,197 B (148,526 B minified) against the
+ // live page's 37,512 (117,301) — the router's contribution +9,685 B br /
+ // +31,225 B minified. Mounted through `dynamicComponent` as the live page
+ // is (the base + router note); with `dynamic`: 49,475 / 155,995. Cap at
+ // measured + 10 B rounded up to 0.01 KB; recorded minified 148,526 B
+ // (CI-confirmed).
+ path: "sc-live-router-app.js",
+ limit: "47.21 KB",
+ capMinified: 148526,
+ alias: pageAlias
+ },
{
name: "server: floor (getRequestEvent + isServer)",
// What a server module that only asks "am I on the server / which