Blazor组件重复渲染时API重复调用问题及优化方案咨询
方案可行性分析与优化建议
一、现有SemaphoreSlim+sessionStorage方案的可行性
这个思路整体是可行的:SemaphoreSlim能有效阻止并发请求,确保同一时间只有一个请求去调用API,其他请求等待缓存生成后直接复用,避免重复请求。但现有代码存在几个关键问题需要修正:
- HttpClient创建问题:每次
GetData方法都新建HttpClient实例,会导致socket资源耗尽,性能低下。正确做法是依赖注入HttpClient而非手动创建。 - IJSInProcessRuntime的使用限制:在Blazor Server场景下,异步方法中调用
IJSInProcessRuntime可能引发线程异常,建议改用IJSRuntime的InvokeAsync方法,兼容Server和WebAssembly环境。 - SemaphoreSlim的static修饰问题:在Blazor Server中,static变量是全局共享的,这会导致不同用户的请求互相等待,造成严重的性能问题。如果是WebAssembly环境,static没问题,但Server端必须改为Scoped级别的锁,或者用更细粒度的键锁。
二、具体优化思路
1. 缓存机制优化
- 环境适配:
- WebAssembly:继续使用sessionStorage,现有缓存键
calendar_{startdate[..4]}设计合理,能保证年度数据的唯一性。 - Blazor Server:建议使用
IMemoryCache(Scoped模式每个用户独立缓存,或Singleton模式键中加入用户ID避免跨用户污染),或者分布式缓存(如Redis),比sessionStorage更适合服务端场景。
- WebAssembly:继续使用sessionStorage,现有缓存键
- 缓存过期:给缓存添加过期时间,比如1天,因为年度日历数据不会频繁变化,既保证数据新鲜度,又减少不必要的API调用。
2. 锁机制优化
推荐用Lazy<Task<List<Calend>>>替代SemaphoreSlim,它本身就是线程安全的,能自动实现“只执行一次初始化逻辑”的需求,代码更简洁:
private readonly ConcurrentDictionary<string, Lazy<Task<List<Calend>>>> _cacheTasks = new(); public async Task<List<Calend>> GetDataByDays(string startdate, string enddate, CancellationToken token = default) { var year = startdate[..4]; var cacheKey = $"calendar_{year}"; // 获取或创建Lazy<Task>,确保同一键只执行一次API调用 var lazyTask = _cacheTasks.GetOrAdd(cacheKey, key => new Lazy<Task<List<Calend>>>(async () => { var dataFromStorage = await _jSRuntime.InvokeAsync<string>("sessionStorage.getItem", key); if (!string.IsNullOrEmpty(dataFromStorage)) { return JsonSerializer.Deserialize<List<Calend>>(dataFromStorage)!; } var calendars = await _httpClient.GetFromJsonAsync<List<Calend>>("./calendar.json", token); await _jSRuntime.InvokeAsync<object>("sessionStorage.setItem", key, JsonSerializer.Serialize(calendars)); return calendars!; })); var fullCalendar = await lazyTask.Value; return fullCalendar.Where(e => string.Compare(startdate[..6], e.CalenYm) <= 0 && string.Compare(enddate[..6], e.CalenYm) >= 0).ToList(); }
这种方式下,同一缓存键的多个并发请求会自动等待第一个请求完成,直接复用结果,无需手动管理SemaphoreSlim的释放,避免遗漏释放导致的死锁。
3. HttpClient优化
在Program.cs中注册HttpClient:
builder.Services.AddHttpClient();
然后在服务构造函数中注入:
private readonly HttpClient _httpClient; private readonly IJSRuntime _jSRuntime; public CalendDataListService(HttpClient httpClient, IJSRuntime jSRuntime) { _httpClient = httpClient; _jSRuntime = jSRuntime; }
4. 组件层面优化
- 提前加载数据:在App.razor或全局Layout的
OnInitializedAsync中调用服务加载数据,然后通过状态容器(如自定义状态类)共享给所有子组件,避免每个组件单独发起请求。 - 控制组件渲染:重写组件的
ShouldRender方法,返回false避免不必要的重新渲染,减少OnInitializedAsync的触发:
protected override bool ShouldRender() { // 只初始化一次,后续不重新渲染 return listData == null; }
5. 异常处理优化
现有异常处理过于简单,建议添加更详细的日志,同时可以考虑缓存降级策略:如果API调用失败,尝试返回缓存中的旧数据(如果存在),而不是直接返回空列表。
内容的提问来源于stack exchange,提问作者Bi Nguyễn
相关产品推荐
相关产品推荐

