Why is this an issue?
In Blazor, using lambda expressions as event handlers when the UI elements are rendered in a loop can lead to negative user experiences and performance issues. This is particularly noticeable when rendering a large number of elements.
The reason behind this is that Blazor rebuilds all lambda expressions within the loop every time the UI elements are rendered.
How to fix it
Ensure to not use a delegate in elements rendered in loops, you can try:
- using a collection of objects containing the delegate as an Action,
- or extracting the elements into a dedicated component and using an EventCallback to call the delegate
Code examples
Noncompliant code example
@for (var i = 1; i < 100; i++)
{
var buttonNumber = i;
<button @onclick="@(e => DoAction(e, buttonNumber))"> @* Noncompliant *@
Button #@buttonNumber
</button>
}
@code {
private void DoAction(MouseEventArgs e, int button)
{
// Do something here
}
}
Compliant solution
@foreach (var button in Buttons)
{
<button @key="button.Id" @onclick="button.Action"> @* Compliant *@
Button #@button.Id
</button>
}
@code {
private List<Button> Buttons { get; set; } = new();
protected override void OnInitialized()
{
for (var i = 0; i < 100; i++)
{
var button = new Button();
button.Action = (e) => DoAction(e, button);
Buttons.Add(button);
}
}
private void DoAction(MouseEventArgs e, Button button)
{
// Do something here
}
private class Button
{
public string? Id { get; } = Guid.NewGuid().ToString();
public Action<MouseEventArgs> Action { get; set; } = e => { };
}
}
Noncompliant code example
@* Component.razor *@
@for (var i = 1; i < 100; i++)
{
var buttonNumber = i;
<button @onclick="@(e => DoAction(e, buttonNumber))"> @* Noncompliant *@
Button #@buttonNumber
</button>
}
@code {
private void DoAction(MouseEventArgs e, int button)
{
// Do something here
}
}
Compliant solution
@* MyButton.razor *@
<button @onclick="OnClickCallback">
@ChildContent
</button>
@code {
[Parameter]
public int Id { get; set; }
[Parameter]
public EventCallback<int> OnClick { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private void OnClickCallback()
{
OnClick.InvokeAsync(Id);
}
}
@* Component.razor *@
@for (var i = 1; i < 100; i++)
{
var buttonNumber = i;
<MyButton Id="buttonNumber" OnClick="DoAction">
Button #@buttonNumber
</MyButton>
}
@code {
private void DoAction(int button)
{
// Do something here
}
}
Resources
Documentation
- Microsoft Learn - ASP.NET Core Blazor performance best practices
- Microsoft Learn - ASP.NET Core Blazor event handling - Lambda expressions
- Microsoft Learn - Event handling - EventCallback Struct
Benchmarks
The results were generated with the help of BenchmarkDotNet and Benchmark.Blazor:
| Method | NbButtonRendered | Mean | StdDev | Ratio |
|---|---|---|---|---|
|
UseDelegate |
10 |
6.603 us |
0.0483 us |
1.00 |
|
UseAction |
10 |
1.994 us |
0.0592 us |
0.29 |
|
UseDelegate |
100 |
50.666 us |
0.5449 us |
1.00 |
|
UseAction |
100 |
2.016 us |
0.0346 us |
0.04 |
|
UseDelegate |
1000 |
512.513 us |
9.7561 us |
1.000 |
|
UseAction |
1000 |
2.005 us |
0.0243 us |
0.004 |
Hardware configuration:
BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 10 (10.0.19045.3448/22H2/2022Update)
12th Gen Intel Core i7-12800H, 1 CPU, 20 logical and 14 physical cores
.NET SDK 8.0.100-rc.1.23463.5
[Host] : .NET 7.0.11 (7.0.1123.42427), X64 RyuJIT AVX2
.NET 7.0 : .NET 7.0.11 (7.0.1123.42427), X64 RyuJIT AVX2