`lazyModule()`: generic lazy-module preload and hydration (not just `lazy()` components)
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 8/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- javascript, rust, typescript
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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)?
- Lenguaje dominante
- TypeScript
- Estrellas
- 36.1k
- Forks
- 1.1k
- Merge medio
- 11 h 34 min
- PR fusionados (30 d)
- 341
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de solidjs/solid
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
Los mantenedores suelen responder en 1 día
-
[diagnostics rc.14 / next] Browser bridge drops shared references, so `expectNoSilentHolds` throws on any capture with a `LONG_HOLD`Posiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 12/100
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 Medio día Aptitud para principiantes 32/100
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 1-2 días Aptitud para principiantes 52/100
Los mantenedores suelen responder en 1 día
Todos los issues de solidjs/solid
Issues similares
-
area/frontend area/v2 kind/bug priority/needs-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
kubeflow/notebooks#1498 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
P1
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
SuruchBoss/Cwork#90 ·
-
bug cli service
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
521xueweihan/HelloGitHub#3922 ·