Skip to content

perf(size): golf client entries before next.31 - #632

Merged
ryansolid merged 1 commit into
nextfrom
golf/client-entries-next31
Sep 28, 2026
Merged

ryansolid merged 1 commit into
nextfrom
golf/client-entries-next31

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Code-size pass on the client entries ahead of 2.0.0-next.31. Every change is behaviour-preserving: the client (jsdom), server and type suites are identical before and after, dev-mode messages are untouched, and .shake-check/check.mjs still passes. Measured against next @ 9600479 (the rebase base), so the numbers reflect what ships.

Sizes

Tooling: tsc && rollup -c output, then terser + brotli (level 11) per entry — the same solid-condition bundling / isServer=false shim / DEV fold the repo's .shake-check scripts use. "Typical app" is a minimal Vite production build (createRouter with three routes incl. one lazy, paths() links, useNavigate, useParams, query, action, createMemo, render), with solid bundled; "router share" is the same app with solid-js / @solidjs/web externalised so only the router's contribution is counted.

entry min before min after Δ brotli before brotli after Δ
. (solid condition, dist/index.jsx) 37 850 37 465 −385 12 792 12 757 −35
. (default, dist/index.js) 37 846 37 461 −385 12 776 12 732 −44
. client-folded (isServer=false, DEV folded) 33 891 33 455 −436 11 607 11 546 −61
./fs 5 450 5 431 −19 2 309 2 306 −3
./server 5 547 5 466 −81 2 273 2 249 −24
typical app — total (solid bundled) 90 369 89 914 −455 30 142 30 030 −112
typical app — router share 28 983 28 525 −458 10 226 10 157 −69

scripts/check-fs-gate.mjs: 104.5 / 125.5 KB → 104.1 / 125.0 KB.

Brotli on inputs this small has roughly ±15 B of noise from context reshuffling, so individual steps are reported with that caveat; the cumulative numbers above are the ones that matter.

Changes (router share of the typical app, min / brotli)

  1. keysFor() reuses pathnameOf(url) (routers/factory.tsx) — it duplicated the pathname derivation match() already does. −44 / −11
  2. ?. for route preload, one-expression segment encoder, flattened paths proxy get (routing.ts, paths.ts) — component?.preload?.() replaces the nested ifs; s => /^[:*]/.test(s) ? s : encodeSegment(s); the symbol branches in paths collapse to two returns. −80 / −8
  3. window.history / history.state aliases (routers/history.ts) — saveCurrentDepth and browserHistory.getSource read the chain once. Brotli-neutral (+2) but −118 min and easier to read; kept for that reason. (Merged with fix: don't throw when history.state is null after replaceState #627's state?._depth ?? length - 1.)
  4. Memo name options behind DEV (routing.ts) — DEV && { name: "lazyRoutes" } etc. Production solid never reads name on memos/signals (only createContext does), so these strings were pure dead weight in prod. −41 / −26
  5. matchSegment / filter lookup (utils.ts) — one ternary chain instead of early-return ifs; filters?.[key] instead of matchFilters && matchFilters[key]. −60 / −21
  6. settle() helper in query() (data/query.ts) — the cached and fresh paths each spelled out the same then/non-thenable settle; now one local function. Brotli-neutral (+4), −21 min; kept because it removes a duplicated branch that has to stay in sync.
  7. Micro folds (routing.ts, data/action.ts, routers/components.tsx, routers/scrollRestoration.ts) — dropped the constant key getter closure in createLocation, disposers[i]?.(), form alias in handleFormAction, response?.data / response?.error, destructured performance.getEntriesByType?.("navigation"). −68 / −13
  8. Trivia (routing.ts, data/action.ts, data/events.ts) — new URL(mockBase) once instead of a separate origin const, fn.with returns toAction(...) directly, flightData keys loop tightened, redundant rel && before rel.includes(...). −11 / −13

Tried and reverted (brotli-negative)

Brotli already compresses literal duplication very well, so extracting shared helpers to dedupe identical code consistently cost bytes on the wire even when it saved minified bytes: a shared isExternalAnchor / isSvg / preloadAnchor for events.ts + claims.ts (−370 min, +18 br), a toggle attribute helper in claims.ts (+8 br), a shared commitEntry for browserHistory / hashHistory (+ on the browser-only typical app), Object.fromEntries in hashKey, an isAsyncIterable helper in liveQuery, spread-gated ...(DEV && { name: "location" }). None are in this PR.

Verification

Run on next @ 9600479 and on this branch — identical:

  • client (jsdom): 40 files / 442 tests
  • server: 6 files / 38 tests
  • test:types: pass
  • .shake-check/check.mjs: all 8 checks ok (outputs reverted, not committed)
  • pnpm build incl. fs gate: pass

Public API changes

None.

Suggestions (not made — need an API decision)

  • scrollRestoration is always bundled. routers/scrollRestoration.ts ships in every client bundle even for apps that pass scrollRestoration: false. Making it an opt-in adapter (like the history adapters) would save every such app its full cost, but changes the createRouter option shape.
  • import("./serverForms.js") in the delegation fallback pulls Vite's __vitePreload helper (~1.1 KB min) into every router app, even ones that never render a server form. A static import or a documented opt-in would remove it; either is a behaviour/API decision, not golf.
  • setupNativeEvents' curried signature and its preload option name (vs preloadLinks on the router config) force an intermediate options object in createRouter (~90 B min). Internal, but tests import it directly, so left alone here.

Deviations from the brief

  • The brief's "typical app" listed Router, Route, A, createAsync. Those exports don't exist on next (v2), so the app uses their v2 equivalents: createRouter with a route array, paths() links on plain <a>, useNavigate, query, action, and createMemo for reads.
  • The fs gate figure includes solid (it bundles solid-js), so it moves less in relative terms than the per-entry numbers.

— Claude via Cursor

DEV-only memo names, a one-expression segment matcher and filter lookup,
one settle helper in query(), keysFor() on match()'s pathname derivation,
and folded one-use closures / repeated property chains. Behavior is
unchanged: client, server and type suites are identical to baseline.

Co-authored-by: Claude <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3fe292d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/router Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ryansolid
ryansolid merged commit 157045e into next Sep 28, 2026
4 checks passed
@ryansolid
ryansolid deleted the golf/client-entries-next31 branch September 28, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant