Problem
@solidjs/router ships a client fallback for server-component forms. These forms arrive as URLs in the HTML with no client reference. When no action() handler is installed, the router intercepts POSTs under actionBase and lazily imports its action layer.
Only server-component apps need this, but every router app pays for it. In a minimal router app with no server functions (Vite 8, gzip):
- about 0.85 KB eager (up to 1.4 KB when it's the app's only dynamic import);
- a 16.8 KB lazy chain: the
serverForms chunk (8.6 KB) plus the decode chunk (7.0 KB) and a server chunk (1.2 KB) behind it.
The router's flat build inlines all of it eagerly (about 8 KB).
Proposal
The plugin defines a global constant __SOLID_SERVER_COMPONENTS__. Core owns and documents the name, with the meaning "server components are enabled for this build".
- Value:
true when serverFunctions.components is truthy (including 'external'), otherwise false. It must always be defined: if it's left undefined, nothing can be removed.
- Set it in both top-level
define (build, and dev source via /@vite/env) and optimizeDeps.rolldownOptions.transform.define (dev pre-bundled dependencies; the optimizer ignores top-level define).
- Respect a user-provided
define[__SOLID_SERVER_COMPONENTS__].
- Applying it to all environments is fine; it's harmless on the server.
Why a define
Whether a check folds away depends on the form:
| Form |
Rollup |
Rolldown 1.2.6 |
esbuild |
Imported, never-written let flag |
folds |
doesn't fold |
doesn't fold |
| Literal define |
folds |
folds |
folds |
Cost
Zero for apps. One config entry in the plugin.
Router follow-up
declare const __SOLID_SERVER_COMPONENTS__: boolean | undefined;
if (typeof __SOLID_SERVER_COMPONENTS__ !== "undefined" && __SOLID_SERVER_COMPONENTS__) {
/* intercept */ import("./serverForms.js")...
}
Bundler, solid entry |
today, KB gz eager / lazy |
with false |
| Rolldown |
18.84 / 16.22 |
17.97 / 0 |
| Rollup |
19.74 / 15.22 |
18.94 / 0 |
| esbuild |
22.62 eager, serverForms lazy |
20.95, serverForms gone |
- With
true, output matches today and serverForms stays lazy, so server-component apps without action() still don't pay for it eagerly.
- In the flat build,
false saves about 7–8 KB eager.
- The router's own build (tsc + Rollup) keeps the guard; no config change is needed.
- Without the plugin, the fallback is off at runtime and the bytes are the same as today. Server-component setups without the plugin define the constant themselves, as documented in core.
Alternatives considered
configureClient setup modules: too much wiring through start mode.
- A runtime flag in server-functions/frames: doesn't fold under Rolldown or esbuild (measured above).
- The router installing the fallback from
serverRouteComponent/action: leaves a coverage gap, because forms can arrive with neither present.
- Asking Rolldown upstream to fold imported constants: out of our control.
Implementation sketch
This goes in the main plugin's config() hook, next to the existing optimizeDeps.rolldownOptions.transform.jsx, using the serverFunctions.components value resolved in solidPlugin().
const SC_KEY = "__SOLID_SERVER_COMPONENTS__";
// inside config():
const scDefine = { [SC_KEY]: userConfig.define?.[SC_KEY] ?? JSON.stringify(serverComponents) };
return {
define: scDefine,
optimizeDeps: {
// ...existing
rolldownOptions: { transform: { jsx: { runtime: "classic" }, define: scDefine }, plugins: [/* existing */] }
}
};
Checklist
Problem
@solidjs/routerships a client fallback for server-component forms. These forms arrive as URLs in the HTML with no client reference. When noaction()handler is installed, the router intercepts POSTs underactionBaseand lazily imports its action layer.Only server-component apps need this, but every router app pays for it. In a minimal router app with no server functions (Vite 8, gzip):
serverFormschunk (8.6 KB) plus the decode chunk (7.0 KB) and a server chunk (1.2 KB) behind it.The router's flat build inlines all of it eagerly (about 8 KB).
Proposal
The plugin defines a global constant
__SOLID_SERVER_COMPONENTS__. Core owns and documents the name, with the meaning "server components are enabled for this build".truewhenserverFunctions.componentsis truthy (including'external'), otherwisefalse. It must always be defined: if it's left undefined, nothing can be removed.define(build, and dev source via/@vite/env) andoptimizeDeps.rolldownOptions.transform.define(dev pre-bundled dependencies; the optimizer ignores top-leveldefine).define[__SOLID_SERVER_COMPONENTS__].Why a define
Whether a check folds away depends on the form:
letflagCost
Zero for apps. One config entry in the plugin.
Router follow-up
solidentryfalseserverFormslazyserverFormsgonetrue, output matches today andserverFormsstays lazy, so server-component apps withoutaction()still don't pay for it eagerly.falsesaves about 7–8 KB eager.Alternatives considered
configureClientsetup modules: too much wiring through start mode.serverRouteComponent/action: leaves a coverage gap, because forms can arrive with neither present.Implementation sketch
This goes in the main plugin's
config()hook, next to the existingoptimizeDeps.rolldownOptions.transform.jsx, using theserverFunctions.componentsvalue resolved insolidPlugin().Checklist
defineplusoptimizeDeps.rolldownOptions.transform.define; respect the user's value.definetrue plus an off-path test; changeset.