diff --git a/aspnetcore/blazor/state-management/server.md b/aspnetcore/blazor/state-management/server.md index cdf44deb59f3..cbcfcd10968c 100644 --- a/aspnetcore/blazor/state-management/server.md +++ b/aspnetcore/blazor/state-management/server.md @@ -6,7 +6,7 @@ description: Learn how to persist user data (state) in server-side Blazor apps. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett ms.custom: mvc -ms.date: 11/11/2025 +ms.date: 07/14/2026 uid: blazor/state-management/server --- # ASP.NET Core Blazor server-side state management @@ -185,6 +185,93 @@ window.addEventListener('visibilitychange', () => { :::moniker range=">= aspnetcore-11.0" +## Automatic circuit pause on tab inactivity + + + +The framework can optionally pause a circuit when the browser tab becomes hidden, freeing server memory and SignalR connections held by inactive users. Auto-pause is provided by the `Microsoft.AspNetCore.Components.Server.AutoPause` package. After adding a package reference, enable the feature by calling `AddAutoPause` when the app's root component is mapped: + +```csharp +app.MapRazorComponents() + .WithBrowserOptions(options => options.AddAutoPause(p => p.HiddenDelay = TimeSpan.FromSeconds(30))); +``` + +`AddAutoPause` configures an `AutoPauseBrowserOptions` instance with the following properties: + +* `Enabled`: Whether auto-pause is enabled. Defaults to `true`, so calling `AddAutoPause` is what enables the feature. +* `HiddenDelay`: The delay after the tab becomes hidden before the circuit pauses. Defaults to two minutes (`TimeSpan.FromMinutes(2)`) and must be greater than `TimeSpan.Zero`. + +After the tab is hidden for `HiddenDelay`, the circuit pauses. If the user returns before the delay elapses, the pause doesn't occur. + +> [!NOTE] +> Auto-pause triggers on the [Page Visibility API](https://developer.mozilla.org/docs/Web/API/Page_Visibility_API) `visibilitychange` event, whose meaning differs by platform: +> +> * On desktop, the tab becomes hidden when the user switches tabs or minimizes the window. The pause timer runs reliably, and the circuit pauses gracefully after the delay. +> * On mobile, the page also becomes hidden when the *whole app* is backgrounded (switching apps, returning to the home screen, or locking the screen), not just when switching browser tabs. +> +> On mobile, the operating system suspends the page's JavaScript shortly after the app is backgrounded (within seconds on Android, up to about 30 seconds on iOS). If `HiddenDelay` is longer than that window, the pause timer never fires, and the circuit is dropped by the OS-initiated disconnect instead of pausing gracefully. The session is still preserved through the normal reconnection and [circuit state persistence](#circuit-state-persistence) path, but the client-side veto and deferral logic doesn't run. For this reason, graceful auto-pause isn't guaranteed and isn't a supported scenario on mobile when the app is backgrounded. + +The framework defers the pause while circuit-owned work is in progress (downloads, uploads, JS interop calls, Web Locks, Picture-in-Picture). It vetoes the pause entirely while focused text `` elements with Blazor `@bind` bindings are edited or audio/video is playing. + +For elements without Blazor bindings (for example, ``, WebRTC connections, or custom elements), the app is responsible for handling state. Register a circuit handler with an `onCircuitPausing` callback in the [Blazor startup configuration](xref:blazor/fundamentals/startup): + +```razor + +``` + +> [!NOTE] +> The `` element can't have its value restored after pause/resume due to browser security restrictions. Using `[PersistentState]` on a property bound to a file `` element causes an `InvalidStateError` that crashes the circuit. Instead, capture the file name in a separate property. Because the browser doesn't expose the file name through the native `change` event, read it with a small JS interop helper: +> +> ```razor +> @inject IJSRuntime JS +> +> +> @SelectedFileName +> +> @code { +> [PersistentState(AllowUpdates = true)] +> public string? SelectedFileName { get; set; } +> +> private async Task HandleFileSelected(ChangeEventArgs e) +> { +> SelectedFileName = await JS.InvokeAsync("getFileName", "persist-upload"); +> } +> } +> ``` +> +> ```javascript +> window.getFileName = (id) => document.getElementById(id)?.files?.[0]?.name ?? ''; +> ``` + +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + ## Server-triggered circuit pause A server-side Blazor app that adopts the Interactive Server render mode can implement server-triggered circuit pause, which allows the app to gracefully pause client circuits, preserving client state for seamless reconnection.