dotnet / dotnet/aspnetcore

[Blazor Hybrid] WebViewRenderer applies no backpressure — unacknowledged render batches accumulate without bound

Open
#68,675 3 comments 1 reaction 0 assignees View on GitHub
area-blazor investigate
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Describe the bug

### Describe the bug

`WebViewRenderer` applies no backpressure. When the JS side stops acknowledging render batches, it keeps rendering, serializing and dispatching new ones without bound. On Android we measured **207 unacknowledged batches** accumulated over a 39-minute period in the background, and the count grows linearly with time.

`UpdateDisplayAsync` enqueues and sends unconditionally, with nothing consulting the depth of `_unacknowledgedRenderBatches`:

https://github.com/dotnet/aspnetcore/blob/v11.0.0-preview.7.26381.103/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs#L47-L59

Blazor Server solves exactly this problem by overriding `ProcessPendingRender()` and refusing to produce a new batch while the buffer is full:

https://github.com/dotnet/aspnetcore/blob/v11.0.0-preview.7.26381.103/src/Components/Server/src/Circuits/RemoteRenderer.cs#L87-L112

...gated on [`CircuitOptions.MaxBufferedUnacknowledgedRenderBatches`](https://github.com/dotnet/aspnetcore/blob/v11.0.0-preview.7.26381.103/src/Components/Server/src/CircuitOptions.cs#L117) (default `10`), documented as *"When the limit of buffered render batches is reached components will stop rendering and will wait until either the circuit is disconnected and disposed or at least one batch gets acknowledged."*

`WebViewRenderer` does not override `ProcessPendingRender()` at all, so it has none of this. The reasoning in `RemoteRenderer`'s comment — that a client which cannot acknowledge fast enough should slow the server down — applies just as well to a WebView, and arguably more: a WebView can stop consuming entirely and for a long time.

Three consequences:

1. **Unbounded retained state.** Each unacknowledged batch keeps a `TaskCompletionSource` alive along with everything registered against it: `InvokeRenderCompletedCallsAfterUpdateDisplayTask` retains an `int[]` of updated component ids, and `RemoveEventHandlerIds` **clones** the disposed event-handler id array and holds the clone until the batch is acknowledged ([Renderer.cs#L1118-L1130](https://github.com/dotnet/aspnetcore/blob/v11.0.0-preview.7.26381.103/src/Components/Components/src/RenderTree/Renderer.cs#L1118-L1130)). The matching entries also stay in `_eventBindings` and `_eventHandlerIdReplacements` for the whole time.

2. **Wasted work on a device that can least afford it.** Rendering, diffing and base64-serializing hundreds of batches for a display that is provably not consuming them is pure cost. On our Android build this contributes to the app being killed by the platform for background CPU use.

3. **It maximises the blast radius of the acknowledgement-desync bug** filed separately. That bug is triggered by a single dropped `RenderBatch` message, and the size of the resulting gap — and therefore the number of batches whose `OnAfterRenderAsync` never fires — is exactly the number of batches allowed to pile up here.

### Expected Behavior

`WebViewRenderer` should apply the same backpressure as `RemoteRenderer`: override `ProcessPendingRender()` and stop producing new batches once the unacknowledged queue reaches a cap, resuming when an acknowledgement frees a slot.

Ideally the cap is configurable (the WebView equivalent of `MaxBufferedUnacknowledgedRenderBatches`), but even a fixed, generous default would bound the retained state and the failure gap.

### Steps To Reproduce

1. Run a .NET MAUI BlazorWebView app on Android that keeps rendering while backgrounded — ours has live data arriving over RPC and produces roughly one render batch every 10 seconds.
2. Send the app to the background. Android demotes the out-of-process WebView renderer (`SandboxedProcessService0`) and, once cached, freezes it. The host app process keeps running and keeps rendering.
3. Leave it backgrounded, then resume and inspect the first acknowledgement.

Observed: on resume the acknowledged batch id was `375` while the queue head was still `169` — 207 batches outstanding after 39 minutes. A shorter ~100-second background period on the same device produced a gap of 10.

Nothing bounds this. The queue depth is simply however many batches the app rendered while the WebView was not consuming.

### Exceptions (if any)

None from this issue directly — unbounded buffering is silent by nature.

It becomes observable through the companion issue, where the first acknowledgement after the gap throws:

```
InvalidOperationException: Received unexpected acknowledgement for render batch 375 (next batch should be 169)
at Microsoft.AspNetCore.Components.WebView.Services.WebViewRenderer.NotifyRenderCompleted(Int64 batchId)
```

The `375` vs `169` in that message is the queue depth this issue is about.

### .NET Version

11.0.100-preview.7.26381.103

### Anything else?

.NET MAUI 11.0.0-preview.7.26406.9, `net11.0-android`, targetSdk 37, Android 16 (Samsung SM-S948U1), out-of-process WebView renderer.

A WebView differs from a SignalR circuit in a way that argues *for* backpressure rather than against it: the host OS can suspend the renderer indefinitely while leaving the .NET side running at full speed, with no disconnect event to notice. Blazor Server's cap has a natural escape hatch — the circuit eventually disconnects and is disposed. A BlazorWebView has none, so an uncapped queue can grow for as long as the user leaves the app in the background.

### Expected Behavior

_No response_

### Steps To Reproduce

_No response_

### Exceptions (if any)

_No response_

### .NET Version

_No response_

### Anything else?

_No response_

Contributor guide

Open the contributing guide

Research direction

Start with src/Components/WebView/WebView/src/Services/WebViewRenderer.cs and compare its render flow with ProcessPendingRender() in src/Components/Server/src/Circuits/RemoteRenderer.cs and the cap in CircuitOptions.cs. Trace acknowledgements through NotifyRenderCompleted, then verify that rendering pauses at a bounded queue depth and resumes after an acknowledgement frees a slot.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, csharp
Domain
mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.