Goal
When a user navigates between Blazor pages via enhanced navigation (no full reload), the existing SignalR circuit should stay attached so subsequent visits to interactive pages (e.g. `/counter`) don't re-pay the ~3.3 s handshake warmup.
Today
BlazorVanilla now ships enhanced navigation via client-side JS (commit `50515f6`). Page transitions between `/`, `/weather` are instant (5–19 ms in browser per agent verification).
But `/counter` specifically falls back to a full reload (commit `f760127`) because splicing the new HTML via `curMain.innerHTML = newMain.innerHTML` leaves the Blazor server-marker comment in the DOM but blazor.web.js doesn't re-run its marker-discovery scan over the new content. Result without the special-case: count never increments after click, button has no event-handler attribute attached (verified in browser).
What does NOT trigger marker rescan
Tested 2026-05-18:
- `document.dispatchEvent(new CustomEvent('enhancedload', ...))` — no effect
- `document.dispatchEvent(new CustomEvent('enhancedNavigationCompleted', ...))` — no effect
- `window.Blazor._internal.attachRootComponents()` — function not exposed on this build
- `MutationObserver` watching for marker comments — fires but no handler picks it up
What's known about blazor.web.js internals
`grep` on `aot/samples/BlazorVanilla/wwwroot/_framework/blazor.web.js` (the embedded build) shows the framework has:
- `attachRootComponentToElement`
- `attachRootComponentToLogicalElement`
- `enhancedNavigationStarted` / `enhancedNavigationCompleted` (events the framework dispatches FROM, not necessarily listens to externally)
- `onDocumentUpdate`
- `localStorage` + `sessionStorage` + `reconnect` / `Reconnect` (so there's reconnect infrastructure too)
The minified source makes parameter shapes opaque. The 'right' API call almost certainly exists but pinning it down needs either:
- Source-map-equipped build of blazor.web.js
- IDE debugging on a working .NET 8/9/10 Blazor Server app
- Reading the upstream framework's TypeScript source at `src/Components/Web.JS/` in dotnet/aspnetcore
Proposed approaches
A. Marker rescan after splice — find the correct `Blazor._internal.*` API to manually trigger discovery, OR replicate what blazor.web.js's own enhanced nav does internally (it must handle this case for the framework's own ``-driven enhanced nav). One reasonable target: simulate a `popstate` event after splice — that's the trigger blazor.web.js uses for its own back/forward enhanced nav. Try and measure.
B. Persistent circuit reconnect — even with full reload between pages, if blazor.web.js can be told to use a stored connectionId from a previous session (via sessionStorage or a query string), the server's existing circuit (still alive in heap) is reused. No handshake. Requires hooking `Blazor.start({ ... })` config or framework-side persistence.
C. Custom -equivalent — implement our own SPA navigator that keeps a single `` always mounted (display:none unless on /counter route). The circuit attaches once on first page load. Show/hide via CSS. Bypasses blazor.web.js's internal marker scan entirely.
Goal scorecard (per `/goal` user goal: pages <1s, warmup once, clicks <2s)
| Goal |
Status |
| All pages open in < 1 s |
✅ PASS (50–200 ms curl, 5–19 ms enhanced nav) |
| Warmup once at first load |
⚠️ PARTIAL — applies to non-Counter transitions; Counter visits still pay 3.3 s warmup each |
| Click round-trip < 2 s |
✅ PASS (~1.7 s slow click) |
This issue tracks closing the Goal 2 gap. Realistic estimate: 1–2 days of framework-source-diving + testing.
File evidence
- Enhanced nav implementation: `aot/samples/BlazorVanilla/Components/App.razor` (lines ~120-210, inline `<script>`)
- Marker-bearing endpoint: `aot/samples/BlazorVanilla/Components/Pages/Counter.razor` + `aot/Wasp.AspNetCore.Blazor.Server/src/WaspMarkers.cs`
- Pre-render registration: `aot/samples/BlazorVanilla/Program.cs:73`
- Browser verification (2026-05-18, agents a12024a/a86a094): counter clicks unwired after splice; full reload restores them
Goal
When a user navigates between Blazor pages via enhanced navigation (no full reload), the existing SignalR circuit should stay attached so subsequent visits to interactive pages (e.g. `/counter`) don't re-pay the ~3.3 s handshake warmup.
Today
BlazorVanilla now ships enhanced navigation via client-side JS (commit `50515f6`). Page transitions between `/`, `/weather` are instant (5–19 ms in browser per agent verification).
But `/counter` specifically falls back to a full reload (commit `f760127`) because splicing the new HTML via `curMain.innerHTML = newMain.innerHTML` leaves the Blazor server-marker comment in the DOM but blazor.web.js doesn't re-run its marker-discovery scan over the new content. Result without the special-case: count never increments after click, button has no event-handler attribute attached (verified in browser).
What does NOT trigger marker rescan
Tested 2026-05-18:
What's known about blazor.web.js internals
`grep` on `aot/samples/BlazorVanilla/wwwroot/_framework/blazor.web.js` (the embedded build) shows the framework has:
The minified source makes parameter shapes opaque. The 'right' API call almost certainly exists but pinning it down needs either:
Proposed approaches
A. Marker rescan after splice — find the correct `Blazor._internal.*` API to manually trigger discovery, OR replicate what blazor.web.js's own enhanced nav does internally (it must handle this case for the framework's own ``-driven enhanced nav). One reasonable target: simulate a `popstate` event after splice — that's the trigger blazor.web.js uses for its own back/forward enhanced nav. Try and measure.
B. Persistent circuit reconnect — even with full reload between pages, if blazor.web.js can be told to use a stored connectionId from a previous session (via sessionStorage or a query string), the server's existing circuit (still alive in heap) is reused. No handshake. Requires hooking `Blazor.start({ ... })` config or framework-side persistence.
C. Custom -equivalent — implement our own SPA navigator that keeps a single `` always mounted (display:none unless on /counter route). The circuit attaches once on first page load. Show/hide via CSS. Bypasses blazor.web.js's internal marker scan entirely.
Goal scorecard (per `/goal` user goal: pages <1s, warmup once, clicks <2s)
This issue tracks closing the Goal 2 gap. Realistic estimate: 1–2 days of framework-source-diving + testing.
File evidence