Skip to content

isRouting is a default-on context read: every app pays ~3 KB br of signals' lane/verdict read path whether or not it asks (pay-for-use isRouting; derive intent/pendingTarget/headed from inflight) #655

Description

@ryansolid

Correction (measured, 2026-10-07 12:30)

The original claim below — that action is statically reached by the router core — holds for the flat default build (dist/index.js, built with inlineDynamicImports: true): there the lazy server-form fallback is inlined, which turns import("./serverForms.js") into a static edge to actionImpl → signals action, installRouterIntegrations and the flight consumer. That is what solidjs/solid#3838's scenarios measured, and it overstates the router by +8,333 min / +2,590 br versus what a Vite app ships. #657 (opened independently) found the same and splits the flat build.

Under the solid export condition — what a Vite app actually resolves (dist/index.jsx + per-module output) — action is already lazy. Measured on examples/hackernews's real client entry through solidjs/solid's size harness:

base + router hackernews
flat default build, stock 184,378 / 57,001 181,895 / 56,084
flat, action's reach removed −11,727 / −3,613 −7,523 / −2,265
solid condition, stock (what Vite ships) 176,045 / 54,411 176,581 / 54,557
solid, server-form fallback dropped (the import-driven shape; #657 keeps it) −3,454 / −1,068 −2,196 / −764
scroll restoration alone −1,141 / −379 −1,141 / −344

What stays in every variant, and why: verdictValue, dissolveLane, laneRead, applyGuesses, isPending, latest, onSettled (≈ 7.6 K min of @solidjs/signals) are retained by the navigation core, not by action: isRouting / intent / the pending target read isPending(source) and latest(source), query reads isPending, and createIntegration uses onSettled. So the remaining question for a read-only app is whether the router's navigation-pending primitive needs the optimistic-lane machinery at all — a different design question from action's tree-shakeability, and the one this issue should now be about.

The router's real marginal on a server-component page is +30,446 min / +9,547 br (not +38,779 / +12,137 as first stated).


Original report follows, kept for context.

Summary

A read-only app (no forms, no mutations — examples/hackernews in solidjs/solid is the canonical case) still bundles the router's action machinery: the form-submit interception in setupNativeEvents, the submission state wired in createRouterContext, action's own glue, and — through it — the optimistic-lane machinery from @solidjs/signals. None of it is reachable from the app's code, but the router core references it statically, so tree-shaking cannot remove it.

Proposal: make action self-installing. The core keeps a tiny seam (a submit hook + a submissions slot); import { action } from "@solidjs/router" registers the form path and the lane-backed submission state into that seam at import time. An app that never imports action bundles none of it. No laziness, no async — plain import-driven tree-shaking, the same shape Solid uses for its own opt-in features.

Measurements

Taken with solidjs/solid's size harness (scripts/size, Rolldown bundle, brotli) on @solidjs/router@2.0.0-next.35 against solid-js / @solidjs/web 2.0.0-rc.13 — see solidjs/solid#3838 (page: base + router scenario). Local macOS / Node 26; minified bytes are exact, brotli ±tens of bytes vs Linux CI.

minified brotli
router standalone (solid/web external) 32,430 11,216
router's marginal cost on a server-component page +38,779 +12,137

Of the +38,779 min marginal:

  • router's own code: 28,008 — createRouterContext 3,029 · setupNativeEvents 2,398 (includes the form-action path) · query cache 1,814 · setupLinkClaims 1,486 · action glue ≈ 2,360 · scroll restoration 788 · browserHistory 594 · …
  • retained from @solidjs/signals: +8,583 — pulled by action: the optimistic lanes (verdictValue, dissolveLane, laneRead), isPending / latest, createEffect, onSettled
  • retained from the server-function client: +1,188 (GET / decodeResponse — query's, legitimately)
  • retained from @solidjs/web: +1,137 (takeHydrationValue, registerElementClaim)
  • retained from solid-js: +364

For a read-only app the action-attributable part is roughly the action glue + the form path inside setupNativeEvents + the signals lane machinery — on the order of 11–13 K minified, ≈ 3–4 KB brotli, carried for a feature the app never imports.

For scale: the router's standalone 11.2 KB br is larger than the entire eager frames (server-components) client after this week's size pass (10.9 KB), and about two-thirds of the whole solid-js + @solidjs/web hydrating runtime.

What "done" looks like

  • An app importing Router / routes / A / useNavigate / query / preload but not action bundles no form-submit interception, no submission state, and none of signals' lane machinery (verdictValue, dissolveLane, laneRead, isPending, latest absent from the bundle unless the app uses them itself).
  • An app that imports action behaves exactly as today (form interception, submissions, optimistic state) — the registration is at import time, synchronous, no behaviour change.
  • Measurable: a variant of solidjs/solid's page: base + router scenario (or an examples/hackernews scenario) with and without import { action }; expected delta ≈ 3–4 KB brotli on the read-only variant.

Secondary candidates (same shape, smaller)

Scroll restoration (788 min) and the form path in setupNativeEvents could follow the same import-driven pattern. query / preload is the read side and should stay in the core.

Method, for reproduction

The attribution came from solidjs/solid's scripts/size/attribute.mjs over the harness bundle (per-function minified bytes; module-level reachability via the bundler's graph). Removing action's reach is measurable on an edited copy of the router's dist/ through the same bundler before any source change — happy to share the scripts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions