Blazor WebAssembly中如何缓存RenderFragment输出?
问题背景
需要实现二维数据虚拟化网格,避免DOM中存在大量元素导致性能下降。核心需求是:视图外的组件可以从DOM中移除,但保留其状态和渲染输出,当进入视图范围时无需重新初始化和渲染,直接恢复到DOM中。
已尝试的方案及局限
通过条件渲染(@if)、@key、RenderFragment等方式测试,发现所有从DOM移除后重新添加的组件都会触发重新初始化和渲染,无法保留原有实例状态;而用display:none隐藏组件虽能保留实例,但组件仍存在于DOM中,无法解决元素过多的性能问题。
测试用组件ConsoleWriter.razor:
<h3>ConsoleWriter @Name</h3> @code { [Parameter] public string? Name { get; set; } protected override void OnInitialized() { Console.WriteLine($"ConsoleWriter {this.Name} initialized"); } protected override void OnAfterRender(bool firstRender) { if(firstRender) { Console.WriteLine($"ConsoleWriter {this.Name} rendered"); } else { Console.WriteLine($"ConsoleWriter {this.Name} re-rendered"); } } protected override bool ShouldRender() => false; }
测试页Index.razor(核心逻辑):
@page "/" <p>Components are showing: @showComponents</p> <button @onclick="() => this.showComponents=!this.showComponents">Toggle</button> @* 隐藏但保留DOM,无法解决性能问题 *@ <div style="@(this.showComponents ? null : "display:none;")"> <ConsoleWriter Name="OnlyHidden" /> </div> @* 条件渲染,每次显示都会重新初始化组件 *@ @if(this.showComponents) { <ConsoleWriter Name="Default" /> @consoleWriter <ConsoleWriter @key="consoleWriterKey" Name="WithKey" /> } @code { private bool showComponents = false; private object consoleWriterKey = new object(); private RenderFragment consoleWriter { get; } = builder => { builder.OpenComponent<ConsoleWriter>(0); builder.AddAttribute(1, nameof(ConsoleWriter.Name), "RenderFragment"); builder.CloseComponent(); }; }
可行解决方案思路
1. 组件实例缓存容器(保留交互性)
创建一个通用缓存容器组件,手动维护子组件的实例,即使子组件不在DOM中,也保留其状态和渲染逻辑,需要显示时直接复用实例。
实现缓存容器ComponentCache.razor
@typeparam TComponent where TComponent : ComponentBase, new() @if (ShouldRender) { @RenderCachedComponent() } @code { private TComponent? _cachedComponent; [Parameter] public bool ShouldRender { get; set; } [Parameter] public Dictionary<string, object?>? ComponentParameters { get; set; } protected override void OnInitialized() { // 初始化组件实例 _cachedComponent = new TComponent(); // 设置组件参数 if (ComponentParameters != null) { var paramView = new ParameterView(ComponentParameters); _cachedComponent.SetParameterAsync(paramView).Wait(); } // 触发组件初始化生命周期 _cachedComponent.AttachToRenderTree(new RenderTreeBuilder(), 0); _cachedComponent.OnInitialized(); } private RenderFragment RenderCachedComponent() => builder => { _cachedComponent?.BuildRenderTree(builder); }; // 更新组件参数方法(可选) public async Task UpdateParameters(Dictionary<string, object?> newParams) { if (_cachedComponent != null) { await _cachedComponent.SetParameterAsync(new ParameterView(newParams)); StateHasChanged(); } } }
使用缓存容器
在Index.razor中替换条件渲染部分:
<ComponentCache TComponent="ConsoleWriter" ShouldRender="showComponents" ComponentParameters="@(new Dictionary<string, object?> { { nameof(ConsoleWriter.Name), "CachedInstance" } })"/>
此方案下,ConsoleWriter实例只会初始化一次,切换显示状态时不会重新触发OnInitialized,仅在首次渲染时触发OnAfterRender。
2. 静态HTML缓存(纯展示场景)
如果组件无需交互,可将组件的渲染结果缓存为HTML字符串,需要显示时直接输出静态HTML,完全避免组件实例的重复初始化。
扩展组件获取渲染HTML
在ConsoleWriter.razor的@code中添加:
public string GetRenderedHtml() { var builder = new RenderTreeBuilder(); BuildRenderTree(builder); var renderer = new HtmlRenderer(new ServiceCollection().BuildServiceProvider()); return renderer.RenderToStringAsync(builder).Result; }
在页面中缓存并使用
@page "/" <p>Components are showing: @showComponents</p> <button @onclick="ToggleShow">Toggle</button> @if (showComponents) { @((MarkupString)_cachedHtml) } @code { private bool showComponents = false; private string _cachedHtml = string.Empty; protected override void OnInitialized() { var component = new ConsoleWriter { Name = "HtmlCached" }; component.OnInitialized(); _cachedHtml = component.GetRenderedHtml(); } private void ToggleShow() => showComponents = !showComponents; }
该方案适合纯展示的虚拟化网格单元格,缺点是丢失组件的交互能力(如按钮点击、绑定更新等)。
3. 改造虚拟滚动组件(虚拟化网格专用)
参考Blazor官方Virtualize组件的实现逻辑,将其“销毁不可见项”的逻辑改为“缓存不可见项实例”:
- 维护一个组件实例缓存池,当项离开视图时,将实例存入缓存池;
- 当项进入视图时,从缓存池取出实例复用,而非创建新实例;
- 仅当缓存池为空时,才创建新的组件实例。
这种方案最适合虚拟化网格场景,既能控制DOM元素数量,又能保留组件的交互性和状态。
内容的提问来源于stack exchange,提问作者Epiphaner

