Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Cerrado
#3,908 0 comentarios 0 reacciones 0 asignados Ver en GitHub

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/_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)?
Lenguaje dominante
TypeScript
Estrellas
36.1k
Forks
1.1k
Merge medio
11 h 34 min
PR fusionados (30 d)
341

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de solidjs/solid

Todos los issues de solidjs/solid

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.