diff --git a/aspnetcore/blazor/state-management/server.md b/aspnetcore/blazor/state-management/server.md index e03bdaaa3b4e..404f439666eb 100644 --- a/aspnetcore/blazor/state-management/server.md +++ b/aspnetcore/blazor/state-management/server.md @@ -5,7 +5,7 @@ author: guardrex description: Learn how to persist user data (state) in server-side Blazor apps. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett -ms.date: 11/11/2025 +ms.date: 07/23/2026 uid: blazor/state-management/server --- # ASP.NET Core Blazor server-side state management @@ -184,6 +184,94 @@ window.addEventListener('visibilitychange', () => { :::moniker range=">= aspnetcore-11.0" +## Automatic circuit pause on tab inactivity + + + +Auto-pause can pause a circuit when the browser tab becomes hidden, freeing server memory and SignalR connections held by inactive users. It's an opt-in feature 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 package 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.