c# Blazor Server 的并发模型和性能问题

来源:这里教程网 时间:2026-02-21 17:37:55 作者:

Blazor Server 的连接模型决定了并发行为

Blazor Server 不是每个请求启动新线程或新实例,而是为每个客户端连接维持一个长期 SignalR 连接,并在该连接上下文中复用

ComponentBase
实例。这意味着:同一用户多次交互(如连续点击按钮)通常复用同一个组件实例;不同用户则对应不同连接和独立的组件生命周期。

关键点在于:

RenderTreeRenderer
会按连接序列化地处理该连接上的所有 UI 更新(包括事件回调、
StateHasChanged()
触发的重渲染),所以单个连接内不存在“并发渲染”——它是严格顺序执行的。

多个用户同时操作 → 多个独立连接 → 各自顺序执行,互不阻塞 同一用户快速连点两次 → 两个事件回调排队进入该连接的消息队列 → 第二个回调必须等第一个完全返回(含异步 await 完成)后才开始
async void
在事件处理中是危险的,会导致框架失去等待时机,可能引发状态错乱或重复提交

常见性能瓶颈:UI 渲染卡顿与服务器资源耗尽

卡顿通常不是因为“并发太高”,而是单次操作耗时过长,阻塞了整个连接的消息循环。典型场景包括:同步 I/O(如

File.ReadAllText()
)、未限制的数据库查询、复杂对象深克隆、或在
OnInitializedAsync()
中执行未分页的全表加载。

更隐蔽的问题是内存泄漏:组件持有大对象(如

byte[]
、缓存的
DataTable
)且未在
Dispose()
中清理,随着连接数增长,服务器内存持续上涨。

避免在组件中缓存未受控的大型数据结构;改用服务层缓存 + 弱引用或 TTL 控制 所有异步操作必须使用
async Task
,禁止
async void
对耗时操作加超时控制,例如:
await httpClient.GetAsync("api/data", cancellationToken).WaitAsync(TimeSpan.FromSeconds(5));
启用
ServerSideCaching
仅适用于静态资源;Blazor Server 本身不缓存组件渲染结果

SignalR 配置直接影响并发承载能力

默认 SignalR 配置在高连接数下容易成为瓶颈。核心参数不是 CPU 或内存,而是 SignalR 的并发连接数限制和消息缓冲区大小。IIS、Kestrel 和 SignalR 自身都有独立的连接/吞吐限制。

例如,Kestrel 默认

MaxConcurrentConnections
为 null(无硬限),但 SignalR 的
MaximumReceiveMessageSize
默认仅 32 KB,上传大参数或 Base64 图片时会直接断连并抛出
BadHttpRequestException: Request body too large

Program.cs
中显式配置 SignalR 选项:
builder.Services.AddSignalR(hubOptions =>
{
    hubOptions.MaximumReceiveMessageSize = 1024 * 1024; // 1 MB
});
Kestrel 需单独配置最大连接数和请求体大小:
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 10000;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
});
IIS 用户需检查
web.config
中的
requestLimits
maxAllowedContentLength

诊断真实瓶颈:别只看 CPU

CPU 占用低但响应慢?大概率是线程池饥饿或 I/O 等待堆积。Blazor Server 的事件处理依赖 .NET 线程池调度,如果大量请求触发长时间同步阻塞(如

Thread.Sleep()
Task.Run(() => { /* CPU-bound */ }).Result
),会迅速耗尽线程池,导致新消息无法及时调度。

dotnet-counters
监控关键指标:
System.Runtime | ThreadPool Queue Length
(持续 > 10 表示调度积压)、
Microsoft.AspNetCore.Hosting | Requests In Progress
(结合连接数判断是否某连接长期占用)。

不要用
.Result
.Wait()
,它们会阻塞线程池线程
CPU 密集型工作必须用
Task.Run()
显式卸载,但要注意避免频繁创建短任务造成调度开销
生产环境务必启用 Application Insights 或 OpenTelemetry,记录
NavigationManager.NavigateTo()
延迟、组件生命周期耗时、SignalR 消息往返时间

实际部署中,最常被忽略的是 SignalR 心跳超时与反向代理(如 Nginx、Azure Front Door)的空闲连接关闭策略不一致,导致连接静默断开却未触发

OnDisconnectedAsync
,用户界面卡死无响应。这个链路层问题比代码逻辑更容易让整个并发模型失效。

相关推荐