

新闻资讯
技术教程Blazor Server 采用单连接顺序执行模型,同一用户交互复用组件实例并串行处理UI更新,多用户则独立并发;性能瓶颈主因是单次操作耗时过长、内存泄漏、SignalR配置不当及线程池饥饿。
Blazor Server 不是每个请求启动新线程或新实例,而是为每个客户端连接维持一个长期 SignalR 连接,并在该连接上下文中复用 ComponentBase 实例。这意味着:同一用户多次交互(如连续点击按钮)通常复用同一个组件实例;不同用户则对应不同连接和独立的组件生命周期。
关键点在于:RenderTreeRenderer 会按连接序列化地处理该连接上的所有 UI 更新(包括事件回调、StateHasChanged() 触发的重渲染),所以单个连接内不存在“并发渲染”——它是严格顺序执行的。
async void 在事件处理中是危险的,会导致框架失去等待时机,可能引发状态错乱或重复提交卡顿通常不是因为“并发太高”,而是单次操作耗时过长,阻塞了整个连接的消息循环。典型场景包括:同步 I/O(如 File.ReadAllText())、未限制的数据库查询、复杂对象深克隆、或在 OnInitializedAsync() 中执行未分页的全表加载。
更隐蔽的问题是内存泄漏:组件持有大对象(如 byte[]、缓存的 DataTable)且未在 Dispose() 中清理,随着连接数增长,服务器内存持续上涨。
async Task,禁止 async void
await httpClient.GetAsync("api/data", cancellationToken).WaitAsync(TimeSpan.FromSeconds(5));ServerSideCaching 仅适用于静态资源;Blazor Server 本身不缓存组件渲染结果默认 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
});builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxConcurrentConnections = 10000;
serverOptions.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
});web.config 中的 requestLimits 和 maxAllowedContentLength
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(),它们会阻塞线程池线程Task.Run() 显式卸载,但要注意避免频繁创建短任务造成调度开销NavigationManager.NavigateTo() 延迟、组件生命周期耗时、SignalR 消息往返时间实际部署中,最常被忽略的是 SignalR 心跳超时与反向代理(如 Nginx、Azure Front Door)的空闲连接关闭策略不一致,导致连接静默断开却未触发 OnDisconnectedAsync,用户界面卡死无响应。这个链路层问题比代码逻辑更容易让整个并发模型失效。