Early hints emit as=script on rel=modulepreload, producing "preloaded but not used" warnings for every route chunk
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Router
Describe the bug
Describe the bug
collectStaticHintsFromManifest in @tanstack/start-server-core hardcodes as: "script" on every script hint, regardless of what rel the manifest helper returned.
getScriptPreloadAttrs in @tanstack/router-core correctly returns no as for the module script format — as is only valid alongside rel=preload, which is the iife branch:
// @tanstack/router-core/dist/esm/manifest.js
function getScriptPreloadAttrs(manifest, link, assetCrossOrigin) {
const preloadLink = resolveManifestAssetLink(link);
const crossOrigin = getAssetCrossOrigin(assetCrossOrigin, "script") ?? preloadLink.crossOrigin;
return {
...getManifestScriptFormat(manifest) === "iife"
? { rel: "preload", as: "script" }
: { rel: "modulepreload" }, // ← no `as`, correctly
href: preloadLink.href,
...crossOrigin ? { crossOrigin } : {},
};
}
But the early-hints caller adds it back unconditionally:
// @tanstack/start-server-core/dist/esm/early-hints.js
const attrs = getScriptPreloadAttrs(manifest, link);
const hint = {
href: attrs.href,
rel: attrs.rel, // "modulepreload" for the module format
as: "script", // ← always added, even when rel is modulepreload
};
The emitted Link header is therefore:
</assets/index-abc123.js>; rel=modulepreload; as=script
Per the HTML spec, modulepreload does not take an as attribute — the destination is implied ("script"). Browsers register a preload that the module loader never matches, and warn a few seconds after load.
Steps to reproduce
- A TanStack Start app with the default module script format and the Link header enabled.
- Load any page and open the console.
Expected behaviour
modulepreload hints are emitted without as, matching what getScriptPreloadAttrs already returns. The hint is honoured and no warning is produced.
Actual behaviour
One warning per route chunk, on every page load. On our app that is 14 lines each load:
The resource https://example.com/assets/index-abc123.js was preloaded using link preload
but not used within a few seconds from the window's load event. Please make sure it wasn't
preloaded for nothing.
Observed in Safari 18. No functional impact — but the console becomes unusable for debugging, which is exactly where you look when something actually breaks.
Suggested fix
Carry as through from the helper instead of hardcoding it:
const attrs = getScriptPreloadAttrs(manifest, link)
const hint = {
href: attrs.href,
rel: attrs.rel,
- as: 'script',
}
+ if (attrs.as !== undefined) hint.as = attrs.as
That keeps the iife path (rel=preload; as=script) unchanged and makes the module path spec-correct. The CSS branch below it is already correct — rel=preload; as=style is valid.
Possible origin
#7327 added the Link header, and #7477 later added the iife/module split in getScriptPreloadAttrs. The as: "script" line in early-hints.ts looks like it predates the split and was not revisited when rel stopped always being preload.
Versions
@tanstack/start-server-core— reproduced in 1.167.22 (our deployed version, warnings observed in production) and the same code is present in 1.169.37@tanstack/router-core1.168.17 / 1.171.32@tanstack/react-start1.167.50@tanstack/react-router1.168.25- Deployed on Cloudflare via Nitro
Workaround
Stripping as=script from modulepreload entries in the Link response header, in our own server entry wrapper. The hint still works — it just becomes valid.
Complete minimal reproducer
see above
Steps to Reproduce the Bug
see above
Expected behavior
see above
Screenshots or Videos
see above
Platform
Not relevant
Additional context
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in the early-hints.ts caller in @tanstack/start-server-core and compare its hint construction with getScriptPreloadAttrs in @tanstack/router-core. Preserve the iife rel=preload; as=script behavior while ensuring modulepreload hints omit as. Done means modulepreload Link headers no longer include as=script and the existing CSS and iife paths remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 90/100