Which project does this relate to?
Router (@tanstack/solid-router + @tanstack/router-core)
Describe the bug
We noticed an occasional Flash of Unstyled Content when navigating between pages on our production build. The app would render briefly with no styles, then snap back to the correct look. It wasn't random, some route transitions always triggered it, others never did. It never happens in vite dev, only in a built app.
Looking in DevTools → Network with "Disable cache" on, we saw the app's CSS file being re-requested on exactly the navigations that flashed. The <link rel="stylesheet"> element in <head> was being removed and a new one with the same href inserted right after, and during the gap it painted an unstyled frame.
Your Example Website or App
https://github.com/jakst/tss-css-reloads
Steps to Reproduce the Bug or Issue
git clone https://github.com/jakst/tss-css-reloads && pnpm install
pnpm build && pnpm preview
- Open the preview URL.
- DevTools → Network → check Disable cache, filter to CSS.
- Click around between Home / A / B / C in the nav.
- Notice that the CSS file is re-requested on some transitions (e.g.
A → B, B → A) and a FOUC is visible on those.
- Optional: right-click the CSS request and throttle its URL to Slow 3G for a longer flash.
Expected behavior
Client-side navigation should leave an unchanged <link rel="stylesheet"> alone. The browser should not be re-fetching the CSS on every nav.
Platform
@tanstack/solid-router@1.168.20
@tanstack/router-core@1.168.15
@tanstack/solid-start@1.167.37
@solidjs/meta@0.29.4
solid-js@1.9.12
- Vite 7.3.2
- macOS, Chromium-family browsers (also reproduces in Safari)
Debugging notes (LLM-assisted — treat as a hypothesis, not confirmed)
I debugged this together with an LLM. The section below is its best-effort read of the code; I haven't independently verified every claim. Posting it in case it's a useful starting point.
The common factor across the transitions that flash is that one route's match chain contributes a different number of modulepreload links than the other's. /b being a parent+child adds an extra preload; /a and /c have the same preload count, which is why A ↔ C doesn't flash.
The LLM pointed at packages/solid-router/src/headContentUtils.tsx, where useTags flattens all head tags into a single positional array and diffs with replaceEqualDeep:
return Solid.createMemo((prev) => {
const next = uniqBy(
[
...meta(),
...preloadLinks(), // length varies per match chain
...links(), // contains <link rel="stylesheet"> from root head()
...styles(),
...headScripts(),
],
(d) => JSON.stringify(d)
)
if (prev === undefined) return next
return replaceEqualDeep(prev, next)
})
replaceEqualDeep compares positionally. When preloadLinks().length differs between prev and next, every tag after the preloads shifts index, so the stylesheet tag object gets a new identity even though an identical tag exists elsewhere in prev.
HeadContent renders these tags via Solid's <For>, which is keyed by strict object identity. When the stylesheet tag loses its identity, <For> disposes the old <Asset> / <Link>. @solidjs/meta's <Link> runs onCleanup, which physically removes the <link rel="stylesheet"> element from the DOM. The replacement <Link> then creates and appends a new one — which is what we see in the Network tab as a fresh CSS request.
Suggested direction (again, LLM hypothesis): replace the positional replaceEqualDeep with an unordered, key-based identity map so any tag whose serialized form appears in prev keeps its identity regardless of position. Something roughly like:
return Solid.createMemo((prev) => {
const nextArr = uniqBy([...], (d) => JSON.stringify(d))
if (!prev) return nextArr
const prevByKey = new Map<string, RouterManagedTag>()
for (const t of prev) prevByKey.set(JSON.stringify(t), t)
let changed = nextArr.length !== prev.length
const result = nextArr.map((t, i) => {
const key = JSON.stringify(t)
const existing = prevByKey.get(key)
if (existing) {
if (existing !== prev[i]) changed = true
return existing // preserve identity regardless of position
}
changed = true
return t
})
return changed ? result : prev
})
Which project does this relate to?
Router (
@tanstack/solid-router+@tanstack/router-core)Describe the bug
We noticed an occasional Flash of Unstyled Content when navigating between pages on our production build. The app would render briefly with no styles, then snap back to the correct look. It wasn't random, some route transitions always triggered it, others never did. It never happens in
vite dev, only in a built app.Looking in DevTools → Network with "Disable cache" on, we saw the app's CSS file being re-requested on exactly the navigations that flashed. The
<link rel="stylesheet">element in<head>was being removed and a new one with the samehrefinserted right after, and during the gap it painted an unstyled frame.Your Example Website or App
https://github.com/jakst/tss-css-reloads
Steps to Reproduce the Bug or Issue
git clone https://github.com/jakst/tss-css-reloads && pnpm installpnpm build && pnpm previewA → B,B → A) and a FOUC is visible on those.Expected behavior
Client-side navigation should leave an unchanged
<link rel="stylesheet">alone. The browser should not be re-fetching the CSS on every nav.Platform
@tanstack/solid-router@1.168.20@tanstack/router-core@1.168.15@tanstack/solid-start@1.167.37@solidjs/meta@0.29.4solid-js@1.9.12Debugging notes (LLM-assisted — treat as a hypothesis, not confirmed)
The common factor across the transitions that flash is that one route's match chain contributes a different number of
modulepreloadlinks than the other's./bbeing a parent+child adds an extra preload;/aand/chave the same preload count, which is whyA ↔ Cdoesn't flash.The LLM pointed at
packages/solid-router/src/headContentUtils.tsx, whereuseTagsflattens all head tags into a single positional array and diffs withreplaceEqualDeep:replaceEqualDeepcompares positionally. WhenpreloadLinks().lengthdiffers between prev and next, every tag after the preloads shifts index, so the stylesheet tag object gets a new identity even though an identical tag exists elsewhere inprev.HeadContentrenders these tags via Solid's<For>, which is keyed by strict object identity. When the stylesheet tag loses its identity,<For>disposes the old<Asset>/<Link>.@solidjs/meta's<Link>runsonCleanup, which physically removes the<link rel="stylesheet">element from the DOM. The replacement<Link>then creates and appends a new one — which is what we see in the Network tab as a fresh CSS request.Suggested direction (again, LLM hypothesis): replace the positional
replaceEqualDeepwith an unordered, key-based identity map so any tag whose serialized form appears inprevkeeps its identity regardless of position. Something roughly like: