Definition
Sometimes on first app entry you want to show a logo splash like a native app from the very first paint, while keeping that splash out of the index of search crawlers that don't execute JavaScript. To do this, instead of server-render (SSR) markup or User-Agent branching, you place an inline blocking script at the very top of the HTML <body> and build the splash node directly into the DOM. Once hydration finishes, the client fades it out with opacity.
An inline blocking script is a <script> embedded directly in the HTML that runs the moment the parser encounters it. A crawler that doesn't run JS won't execute it, so the splash never remains in the HTML the bot sees.
Why It's Needed
This pattern is needed because three alternatives are each blocked.
- SSR markup puts the splash right into the first HTML, so it leaks into crawler indexes (unwanted loading text shows up in search results).
- A UA gate (reading request headers on the server to "skip the splash if it's a bot") forces the static generation/ISR of routes below it into dynamic, invalidating the entire static cache the moment you read
headers()in a shared layout. - A client splash that appears only after mount leaves the server's initial state empty, so the first HTML is blank; as a "first loading screen" it creates a first-paint gap and is unsuitable.
Inline script injection avoids all three problems at once by keeping static render/ISR intact and drawing the splash only at JS execution time. Here, ISR (Incremental Static Regeneration) is a caching approach that pre-bakes the page statically and only refreshes it periodically, and forcing dynamic means the server re-renders on every request.
How It Works
- Modern React (19) tolerantly skips unexpected nodes inserted at the top of body/head by third parties (browser extensions, scripts) during hydration. Thanks to this, injecting without SSR doesn't cause a hydration mismatch.
- The splash node is outside the React tree (created by the script), so even if the client removes it directly from the DOM there's no reconciliation conflict.
Practical Application
After hydration the client sets opacity 0 on .boot-splash and removes it when the transition ends.
Trade-offs
- Inline script injection: keeps the static cache/ISR and gets both bot hiding and first-paint reveal. In exchange, you build the script as a string, so you must be careful with escaping and testing.
- SSR markup: easiest to implement but leaks into the bot index.
- UA gate: hides from bots but breaks the static cache.
When Not to Use It
- When bot hiding isn't needed — a plain SSR/CSS splash is simpler.
- A purely dynamic app with no static/ISR cache on any sub-route — there's no cache to lose even with a UA gate, so the benefit of this pattern shrinks.
Common Mistakes
- Carelessly using
headers()/cookies()in a shared layout and breaking sub-route ISR into dynamic. - Mixing
</script>into the string put in the inline script, terminating the script early (prevent with escaping/textContent, verify with boundary-value tests). - Setting the splash background color differently from body, causing a color jump the moment it fades.
Related Concepts
- ssr-hydration-mismatch — the premise of how hydration handles injected nodes
- usesyncexternalstore-snapshot-identity — mount-gated client state that's empty on the server
- critical-rendering-path — the impact of a blocking script on parsing and paint