`lazyModule()`: generic lazy-module preload and hydration (not just `lazy()` components)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 8/100
- Tipo di issue
- Funzionalità
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- javascript, rust, typescript
Direzione di ricerca
This is a design proposal, so start by settling the open questions rather than writing code. Read the lazy transform in packages/compiler/src/lazy.rs, then the server registration in packages/solid/src/server/component.ts and the client lookup in packages/solid/src/client/hydration.ts. Done means agreed answers on implicit peek() versus claim(), key namespacing, and fail-soft misses, with maintainer sign-off.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Problem
lazy() is the only code-split value that SSR knows how to preload before hydration. Libraries that lazy-load something other than a component get none of this. The motivating case is router lazy subtrees: children: () => import("./admin/routes").
On a hard load into such a subtree, the server loads the route table and renders it. The client usually hasn't loaded that chunk when hydration reaches it. The router parks, <Loading> replaces the server HTML with its fallback, and hydration hits key misses.
Most of the machinery lazy() uses is already generic:
- Manifest lookup and asset registration (
resolveAssets,registerAsset), with modulepreload and CSS emission. registerModule(key, entryUrl), which treats the key as opaque and files it under the current boundary (packages/web/src/server.ts:551–559).- Client fill:
loadModuleAssets(mapping)fills_$HY.modules[key]and treats keys as opaque (packages/web/src/client.ts:2162–2186). - Hydration waits:
hydrate()waits on the root<renderId>_assets/_assetsmap (client.ts:2295–2298), and boundaries wait on their own asset maps (packages/solid/src/client/hydration.ts:3063).
Three things are specific to lazy:
- The call shape the transform matches.
packages/compiler/src/lazy.rs(1–18, 120–131) only matcheslazy(...)/clientOnly(...)imported by name fromsolid-js/@solidjs/web, with a literalimport()thunk. It appends"__SOLID_LAZY_MODULE__:<spec>". - Non-JSX files get nothing. The plugin's
transformreturns early for files that aren't.jsx/.tsx(only primitive naming runs there). So aroutes.tsgets neither the lazy pass nor the server-side$$moduleUrlexport (injectSsrModuleId). Today this also meanslazy()written in a.tsfile isn't annotated. - Keys are hydration ids. Server
lazyregisters underpeekNextChildId(owner)(packages/solid/src/server/component.ts:317, 336). The client recomputes the same id by position inlazyHydrationLookup(client/hydration.ts:1726–1774). Any owner that exists on only one side shifts the ids. solid-router recently fixed a router-internal version of this drift: a client-only reader was taking a hydration id the server never allocates, fixed by making that memotransparent(solidjs/solid-router@51433a3).
Proposal: lazyModule from solid-js
const admin = lazyModule(() => import("./admin/routes"));
admin(); // Promise<Module>, cached (assignable to () => Promise<M>)
admin.preload(); // same cached load (parity with lazy().preload)
admin.peek(); // Module | undefined, synchronous
admin.moduleUrl; // injected by the transform
- Transform. It matches
lazyModuleexactly likelazy(named import fromsolid-js, literal import thunk) and appends the same placeholder. The plugin also runs the lazy pass on.ts/.jsfiles that contain the stringlazyModule(a cheap substring check). - Registration happens per render, not per load.
- On the server, reading
peek()during render resolves assets, registers the preload, and callsregisterModule(moduleUrl, entry)under the caller's boundary. While the module is loading, it blocks the shell throughctx.block, the same aslazy. preload()never registers. This matches today'slazy().preload, which stays hint-only (server/component.ts:455–457).- Open question: should registration be implicit in
peek()or a separateclaim()? We lean towardpeek().
- On the server, reading
- Client. During hydration,
peek()returns_$HY.modules[moduleUrl]. - Keys are module ids, not hydration ids. Module ids don't depend on owner position. Both key kinds share
_$HY.modules. They have different shapes (path vs0-1-2), but a prefix might still be worth adding. lazy()is unchanged. Conceptually,lazyislazyModuleplus a component wrapper, which is why this primitive is general enough. Rebuilding it that way isn't proposed: building it onlazyModule+dynamicComponent(#3907) measured about +460 B gzipped for apps that uselazybut notdynamicComponent, and changes the owner shape under everylazy. Core can share internals between them only where that doesn't add bytes.- Unmapped modules (computed specifiers,
import.meta.glob, thunks the transform can't see):- On the server, read
$$moduleUrloff the loaded module, the same waylazy's glob path does (server/component.ts:344–383). This requires injecting$$moduleUrlinto all server-build project modules, not only JSX files. It's still skipped fornode_modules. - In dev, warn when a
lazyModuleends up with no id: the call itself signals that preloading was intended.
- On the server, read
- Misses fail soft. A key miss makes
peek()returnundefined, which gives today's behavior and never a new failure. So server/client transform coverage gaps (for example SSR-externalized dependencies) can't make things worse. By contrast,lazywith amoduleUrlthrows on a miss (hydration.ts:1753–1771). - Libraries can require it in their types. For example, the router could type lazy
childrenas a brandedLazyModule<T>and documentchildren: lazyModule(() => import("./admin/routes")). It could also dev-warn when a plain thunk parks during hydration. - Router sketch.
- Server:
<Router>claims the matched lazy boundaries on every request (root map). - Client:
createRoutescallspeek()before creating a placeholder.
- Server:
Other use cases
- CMS/page-builder block registries
- Content collections (MDX entries that carry components)
- Plugin/widget registries
- Form renderers with custom field types
All of these resolve "which module" from data, then render what it exports.
Note: await import() inside an async memo. An async memo whose value isn't serializable (a module namespace, a function) has no supported hydration path today. The server serializes async memo landings through ctx.serialize. Seroval has no encoding for functions: it throws at the write, or reports to onError as handling: "serialize" when a hook is passed (packages/web/src/server.ts:6327–6339). dynamic documents the client effect as "a record the serializer cannot write — would leave the client waiting on it forever" (index.server.ts:296–310). We haven't verified the end-to-end client behavior for a plain createMemo(async () => (await import(x)).default). That is an open question. lazyModule gives that pattern a synchronous path: read m.peek() and stay sync.
Cost
- Apps that don't use
lazyModulepay nothing. lazyModulealone measured about +90 B gzipped on the client.- Transform cost: a substring check on
.ts/.jsfiles, plus the$$moduleUrlexport in server-build modules (server-only).
Rejected alternatives
- Implicitly tag every
() => import("lit")thunk. This makes registration an invisible side channel with a silent fallback, and it costs every app bytes. The hard cases (computed specifiers, glob, inlineawait import()) aren't literal thunks anyway. - Wrap every
import()call. Cached loads register only on the first request, and server-only imports would get registered. - Library-chosen keys,
registerModule(key, promise). Every library would invent its own keys, and the client would still need a way to look them up.
Prior art
- Loading matched lazy routes before hydrating: React Router/Remix (route modules for the initial matches) and TanStack Router (route chunks for the initial matches).
- Attaching module identity at transform time: Qwik's optimizer gives extracted symbols stable ids, and Vite's build rewrites dynamic imports with
__vitePreload(() => import(...), deps).
Open questions
- Should registration be implicit in
peek()or a separateclaim()? - Should keys be namespaced inside
_$HY.modules? - Should
lazy's hard throw on a miss become fail-soft? - What does the plain async-memo
import()pattern do on the client in practice (see the note above)?
- Lingua principale
- TypeScript
- Stelle
- 36.1k
- Fork
- 1.1k
- Merge medio
- 9h 56m
- PR unite (30g)
- 336
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di solidjs/solid
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 12/100
solidjs/solid#3975 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 3/5 Mezza giornata Idoneità per principianti 32/100
solidjs/solid#3974 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 52/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di solidjs/solid
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
microsoft/playwright#43327 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
microsoft/TypeScript#64758 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
opensearch-project/OpenSearch-Dashboards#12937 ·
I maintainer di solito rispondono entro 2 giorni
-
Liveness marks block-included attesters live in the block's epoch instead of the attestation's epochAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
I maintainer di solito rispondono entro 1 giorno
-
status: needs-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 83/100
a2ui-project/a2ui#3158 ·
I maintainer di solito rispondono entro 2 giorni