feat(web): add typed preload links to the SSR asset pipeline - #3162
Conversation
🦋 Changeset detectedLatest commit: d571f8b The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will regress 1 benchmark
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | merge |
71.9 µs | 138.5 µs | -48.05% |
| ⚡ | merge |
274.9 µs | 186 µs | +47.82% |
| ⚡ | merge |
366.9 µs | 265.4 µs | +38.28% |
| ⚡ | merge |
358.7 µs | 265.4 µs | +35.17% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing v-sayapin:feat/typed-preload-links (d571f8b) with next (d813a96)
Footnotes
-
132 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
ryansolid
left a comment
There was a problem hiding this comment.
Reviewed in depth — the substance is right and this completes the #3031 typed-resource thread the way we intended when dom-expressions#582 was parked. The explicit-links-only policy (no destination guessing, no assets auto-preload, no dynamicImports traversal) is exactly the stance we want, the HTMLPreloadAs six-destination split matches the fetch spec's preload destinations, and the first-registration-metadata vs identity split mirrors the browser's preload cache key. The frames identity-consumption + warm-store accumulation interaction also traces correctly for both mounted and late-mount paths.
Three changes needed:
-
Trim the manifest type to our contract. Drop
assets?: string[]anddynamicImports?: string[]fromAssetManifest(bothserver.tsandserver-mock.ts). The runtime deliberately never reads them, and the manifest type is our contract, not a Vite-compat surface — we may feed this from non-Vite bundlers, and extra fields on parsed JSON pass through structurally without being in the type. Keep exactly what the pipeline consumes:file,css,imports,isEntry,preloads,_base. -
Rebase onto current
next. The size-limit failure isn't your measurement being wrong — your branch measured green, but recent landings onnext(optimistic-store replay work, among others) share the same budgets, and the merge ref tips four scenarios over by 11–49 bytes. -
Re-measure and set the budgets consciously on the rebased branch — including the frames client limit you already bumped (11.22 kB will no longer clear; CI's merge measurement was 11.27 kB).
The local diagnostics.spec.tsx failure you'd see on the current branch is a stale-base artifact (the validate-error compiler change landed with its test fix after your base) — the rebase resolves it.
|
Pushed the requested changes directly (maintainer edits):
|
47b1949 to
d571f8b
Compare
Summary
This reopens the work from dom-expressions#582 in its new home after the DOM runtime was absorbed into the Solid monorepo. It also completes the typed-resource follow-up from #3031, which taught
lazy().preload()to register a module's full static JS/CSS closure. The remaining gap was resources whose request cannot be described by a URL alone.The SSR asset pipeline tracked stylesheets and modulepreloads as bare URLs and inferred the link kind from the
.csssuffix. That works for JS and CSS, but leaves no way to carry fonts, images, MIME types, CORS mode, SRI, or fetch priority through the same pipeline.Static manifests and
AssetResolverresults can now attach explicitPreloadLink[]entries:The links are forwarded through the entry manifest,
lazy()'s render andpreload()paths, its synchronousmoduleUrlpath for islands, andclientOnly(). UnderNoHydration, CSS and non-script preloads are kept while script preloads are skipped unlessmoduleUrlaccess explicitly signals that the client will load the module.as, MIME type, CORS mode, integrity, referrer policy, fetch priority, and media surviverenderToString, streaming, embedded-head and custom-sink output, as well as frame transport and late frame delivery. Script and style preloads also receive the matching split CSP nonce.Links share the existing head resource identity set, so a manifest link and a
useHeadlink dedupe in either registration order.integrity,referrerpolicy, andfetchpriorityremain first-registration metadata rather than part of the preload identity; the browser's reuse key is determined by the URL, destination, mode, and credentials mode.Preloads stay explicit. Raw manifest
assetsare not preloaded automatically anddynamicImportsare not traversed, so the runtime never guesses a request destination or defeats code splitting. The integration that knows the asset type and preload policy selects the links.The existing stylesheet/modulepreload path stays unchanged: it still uses
Set<string>, and the typed-link array is allocated only when the first link is registered. Validation and identity dedupe happen before the document and frame output forms are built.This also exports the six HTML preload destinations as
JSX.HTMLPreloadAsand fetch priority asJSX.HTMLFetchPriorityfor reuse across the public types.The second commit fixes late root assets in frames. Root asset chunks share one wire key, so each arrival previously replaced the retained record and a frame mounted later could see only the last one. The warm store now extends the retained record in place and consumes every arrival.
Responsive image preloads (
imagesrcset/imagesizes, including the standard form withouthref) remain a separate follow-up.How did you test this change?
Added runtime and type coverage for:
_baseresolution through the static import graph whileassetsanddynamicImportsremain untoucheduseHeadin both registration orders and conflicting first-registration metadatalazy()render,lazy().preload(),moduleUrl,clientOnly(), andNoHydrationValidation:
pnpm exec turbo run build test test-types typecheck --filter=solid-js --filter=@solidjs/web --filter=@solidjs/h --force— 14/14 tasks passed, 0 cachedpnpm test— 33/33 workspace tasks passedgit diff --checkpassedOne generator note:
pnpm --filter @solidjs/web run jsx-syncalso removes a$keyblock that is present injsx.d.tsbut missing fromjsx-h.d.ts. That drift predates this branch and is intentionally not included here.