Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

`lazyModule()`: generic lazy-module preload and hydration (not just `lazy()` components)

Chiusa
#3,908 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

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/_assets map (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:

  1. The call shape the transform matches. packages/compiler/src/lazy.rs (1–18, 120–131) only matches lazy(...)/clientOnly(...) imported by name from solid-js/@solidjs/web, with a literal import() thunk. It appends "__SOLID_LAZY_MODULE__:<spec>".
  2. Non-JSX files get nothing. The plugin's transform returns early for files that aren't .jsx/.tsx (only primitive naming runs there). So a routes.ts gets neither the lazy pass nor the server-side $$moduleUrl export (injectSsrModuleId). Today this also means lazy() written in a .ts file isn't annotated.
  3. Keys are hydration ids. Server lazy registers under peekNextChildId(owner) (packages/solid/src/server/component.ts:317, 336). The client recomputes the same id by position in lazyHydrationLookup (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 memo transparent (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 lazyModule exactly like lazy (named import from solid-js, literal import thunk) and appends the same placeholder. The plugin also runs the lazy pass on .ts/.js files that contain the string lazyModule (a cheap substring check).
  • Registration happens per render, not per load.
    • On the server, reading peek() during render resolves assets, registers the preload, and calls registerModule(moduleUrl, entry) under the caller's boundary. While the module is loading, it blocks the shell through ctx.block, the same as lazy.
    • preload() never registers. This matches today's lazy().preload, which stays hint-only (server/component.ts:455–457).
    • Open question: should registration be implicit in peek() or a separate claim()? We lean toward peek().
  • 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 vs 0-1-2), but a prefix might still be worth adding.
  • lazy() is unchanged. Conceptually, lazy is lazyModule plus a component wrapper, which is why this primitive is general enough. Rebuilding it that way isn't proposed: building it on lazyModule + dynamicComponent (#3907) measured about +460 B gzipped for apps that use lazy but not dynamicComponent, and changes the owner shape under every lazy. 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 $$moduleUrl off the loaded module, the same way lazy's glob path does (server/component.ts:344–383). This requires injecting $$moduleUrl into all server-build project modules, not only JSX files. It's still skipped for node_modules.
    • In dev, warn when a lazyModule ends up with no id: the call itself signals that preloading was intended.
  • Misses fail soft. A key miss makes peek() return undefined, 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, lazy with a moduleUrl throws on a miss (hydration.ts:1753–1771).
  • Libraries can require it in their types. For example, the router could type lazy children as a branded LazyModule<T> and document children: 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: createRoutes calls peek() before creating a placeholder.
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 lazyModule pay nothing.
  • lazyModule alone measured about +90 B gzipped on the client.
  • Transform cost: a substring check on .ts/.js files, plus the $$moduleUrl export 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, inline await 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
  1. Should registration be implicit in peek() or a separate claim()?
  2. Should keys be namespaced inside _$HY.modules?
  3. Should lazy's hard throw on a miss become fail-soft?
  4. 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

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di solidjs/solid

Tutte le issue di solidjs/solid

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.