176 lines
4.9 KiB
Plaintext
176 lines
4.9 KiB
Plaintext
== Why is this an issue?
|
|
|
|
In Blazor, using https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling#lambda-expressions[lambda expressions] as https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling#lambda-expressions[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 https://learn.microsoft.com/en-us/dotnet/api/system.action[Action],
|
|
* or extracting the elements into a dedicated component and using an https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling#eventcallback[EventCallback] to call the delegate
|
|
|
|
=== Code examples
|
|
|
|
==== Noncompliant code example
|
|
|
|
[source,csharp,diff-id=1,diff-type=noncompliant]
|
|
----
|
|
@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
|
|
|
|
[source,csharp,diff-id=1,diff-type=compliant]
|
|
----
|
|
@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
|
|
|
|
[source,csharp,diff-id=2,diff-type=noncompliant]
|
|
----
|
|
@* 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
|
|
|
|
[source,csharp,diff-id=2,diff-type=compliant]
|
|
----
|
|
@* 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 - https://learn.microsoft.com/en-us/aspnet/core/blazor/performance#avoid-recreating-delegates-for-many-repeated-elements-or-components[ASP.NET Core Blazor performance best practices]
|
|
* Microsoft Learn - https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling#lambda-expressions[ASP.NET Core Blazor event handling - Lambda expressions]
|
|
* Microsoft Learn - https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling#eventcallback[Event handling - EventCallback Struct]
|
|
|
|
=== Benchmarks
|
|
|
|
The results were generated with the help of https://github.com/dotnet/BenchmarkDotNet[BenchmarkDotNet] and https://github.com/egil/Benchmark.Blazor/tree/main[Benchmark.Blazor]:
|
|
|
|
[options="header"]
|
|
|===
|
|
| 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:
|
|
|
|
[source,text]
|
|
----
|
|
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
|
|
----
|