HeadOutlet with InteractiveServer removes all your CSS during the prerender transition - here's the fix
I finally tracked down a FOUC (Flash of Unstyled Content) issue that's been bugging me in Blazor Server and I suspect a lot of people are hitting this without realizing the root cause.
The Problem
If you're using <HeadOutlet @rendermode="InteractiveServer" /> (which is the default Blazor Server template), there's a hidden behavior during the prerender-to-interactive transition: HeadOutlet removes ALL <link> tags rendered via <HeadContent> from <head>, then re-adds them after the interactive render completes.
On localhost you barely notice. To reproduce it, open DevTools > Network > throttle to "Slow 3G" or "Fast 3G" and reload. You'll see your fully styled prerendered page flash to completely unstyled content, then snap back. On real-world mobile connections this is exactly what your users experience.
Why the standard loading overlay approach doesn't work
You might think "I'll just show a loading overlay until the circuit connects." The problem is that afterServerStarted fires before HeadOutlet starts its remove/re-add cycle. So the overlay hides, then your CSS gets ripped out.
The Fix
Three parts:
1. Move your main stylesheet out of HeadContent
Put app.min.css as a static <link> directly in App.razor, before HeadOutlet:
<head>
<base href="/" />
<link href="/css/app.min.css" rel="stylesheet" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
Static tags in App.razor are never touched by HeadOutlet. Only tags rendered through <HeadContent> get removed and re-added. This alone prevents the worst of it.
2. Overlay that understands the HeadOutlet lifecycle
In your JS initializer ({AssemblyName}.lib.module.js), use afterServerStarted to re-show the overlay, then attach a MutationObserver to document.head watching for <link> removals followed by re-additions. Only hide the overlay after HeadOutlet has finished its cycle and settled.
3. Wait for stylesheets to actually load
Even after HeadOutlet re-adds the <link> tags, the browser may not have downloaded them yet. Before hiding the overlay, check that every <link rel="stylesheet"> has a .sheet property (meaning it's parsed and ready). This prevents the overlay from hiding while CSS is still loading.
Gotcha
If your overlay has a safety timeout that sets a hidden flag (to prevent double-hides), make sure re-showing the overlay resets that state. I had a bug where the timeout fired, set hidden = true, and then when afterServerStarted tried to hide the overlay later it was a no-op. The overlay stayed up forever on slow connections.
Result
Zero layout shift. Works whether the circuit connects in under a second or takes 15 seconds on a bad connection. The page stays behind the overlay until everything is genuinely ready.
I'm surprised this isn't documented more prominently given it affects every Blazor Server app using HeadOutlet with prerendering. Hope this saves someone the debugging time.
Posted Feb 7, 2026, 10:57 PM UTC