Поделиться через


Лучшие практики по повышению производительности рендеринга в ASP.NET Core Blazor

Замечание

Это не последняя версия этой статьи. Для текущей версии см. версию .NET 9 этой статьи.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Для получения дополнительной информации см. Политику поддержки .NET и .NET Core. Для текущей версии см. версию .NET 9 этой статьи.

Это важно

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Корпорация Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых в отношении информации, предоставленной здесь.

Для текущей версии см. версию .NET 9 этой статьи.

Оптимизируйте скорость отрисовки, чтобы свести к минимуму связанную с отрисовкой рабочую нагрузку и повысить скорость реагирования пользовательского интерфейса, что может привести к увеличению скорости отрисовки пользовательского интерфейса в десять раз или даже больше.

Избегайте ненужной отрисовки поддеревьев компонентов

Вы можете исключить большую часть затрат ресурсов на отрисовку родительского компонента, пропустив отрисовку поддеревьев дочернего компонента при наступлении события. При этом достаточно лишь обеспечить пропуск отрисовки для тех поддеревьев, отрисовка которых особенно трудоемка и может приводить к задержкам в пользовательском интерфейсе.

В среде выполнения все компоненты занимают определенное место в иерархии. Корневой компонент (первый загруженный компонент) содержит дочерние компоненты. Каждый из этих дочерних элементов может содержать собственные дочерние компоненты, и так далее. При возникновении события, например при нажатии кнопки пользователем, выбор компонентов для отрисовки осуществляется по следующему алгоритму.

  1. Возникшее событие отправляется в тот компонент, который создал обработчик такого события. После выполнения обработчика событий соответствующий компонент повторно отрисовывается.
  2. При повторной отрисовке компонент предоставляет новую копию значений параметров каждому из своих дочерних компонентов.
  3. После получения нового набора значений параметров Blazor решает, следует ли перерендерить компонент. Компоненты перерисовываются, если ShouldRender возвращает true, что является поведением по умолчанию, если это не переопределено, а значения параметров могут измениться, например, если они являются изменяемыми объектами.

Далее два последних шага рекурсивно повторяются для всех компонентов вниз по иерархии. Во многих случаях это приводит к перерисовке всего дерева. События для компонентов высокого уровня могут приводить к высоким затратам на повторную отрисовку всех компонентов вниз по иерархии, начиная с целевого.

Избежать рекурсивной повторной отрисовки всех компонентов в поддереве можно одним из следующих способов.

  • Убедитесь, что дочерние параметры компонента имеют определенные неизменяемые типы†, например string, , intboolи DateTime. Встроенная логика обнаружения изменений автоматически пропускает rerendering, если неизменяемые значения параметров не изменились. Если для отрисовки дочернего компонента используется <Customer CustomerId="item.CustomerId" />, где значение CustomerId имеет тип int, то компонент Customer будет повторно отрисовываться только при изменении item.CustomerId.
  • Переопределить ShouldRender, чтобы вернуть false:
    • Если параметры являются непримитивными типами или неподдерживаемыми неизменяемыми типами†, например сложными настраиваемыми типами моделей или значениями RenderFragment, и значения параметров не изменились,
    • При создании компонента, который предназначен только для пользовательского интерфейса и не изменяется после первоначальной отрисовки, независимо от изменения значения параметра.

† Для получения дополнительных сведений см. логику обнаружения изменений в Blazorсправочном источнике (ChangeDetection.cs).

Замечание

Ссылки в документации на исходный код .NET обычно загружают ветку репозитория по умолчанию, которая представляет текущую разработку для следующего выпуска .NET. Чтобы выбрать тег для конкретного релиза, используйте раскрывающийся список Переключение ветвей или тегов. Дополнительные сведения см. в статье Выбор тега версии исходного кода ASP.NET Core (dotnet/AspNetCore.Docs #26205).

В следующем примере средства для поиска авиарейсов используются закрытые поля для отслеживания необходимой информации для обнаружения изменений. Идентификаторы предыдущего прибывающего (prevInboundFlightId) и отбывающего (prevOutboundFlightId) рейсов используются для отслеживания информации для следующего возможного обновления компонента. В случае изменения любого из этих идентификаторов рейса при установке параметров компонента в OnParametersSet выполняется повторная отрисовка компонента, поскольку параметр shouldRender имеет значение true. Если после проверки идентификаторов рейса параметр shouldRender имеет значение false, ресурсоемкая повторная отрисовка не производится.

@code {
    private int prevInboundFlightId = 0;
    private int prevOutboundFlightId = 0;
    private bool shouldRender;

    [Parameter]
    public FlightInfo? InboundFlight { get; set; }

    [Parameter]
    public FlightInfo? OutboundFlight { get; set; }

    protected override void OnParametersSet()
    {
        shouldRender = InboundFlight?.FlightId != prevInboundFlightId
            || OutboundFlight?.FlightId != prevOutboundFlightId;

        prevInboundFlightId = InboundFlight?.FlightId ?? 0;
        prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
    }

    protected override bool ShouldRender() => shouldRender;
}

Обработчик событий также может присваивать параметру shouldRender значение true. Для большинства компонентов определение необходимости в повторной отрисовке на уровне отдельных обработчиков событий, как правило, не требуется.

Дополнительные сведения см. в следующих ресурсах:

Виртуализация

Если в некотором цикле выполняется отрисовка значительной части пользовательского интерфейса, например списка или сетки с тысячами записей, само по себе количество операций может приводить к задержке в отрисовке пользовательского интерфейса. Учитывая, что пользователь одновременно без прокрутки видит лишь небольшое количество таких элементов, зачастую излишне тратить время на отрисовку тех элементов, которые пока не видны.

В Blazor предоставляется встроенный компонент Virtualize<TItem>, который эмулирует поведение отображения и прокрутки для произвольно большого списка элементов, фактически отрисовывая только те из них, которые попадают в текущее окно просмотра с учетом прокрутки. Например, компонент может отрисовывать список из 100 000 записей, тратя при этом ресурсы отрисовки только на те 20 элементов, которые видимы в конкретный момент времени.

Дополнительные сведения см. в статье Виртуализация компонентов Razor ASP.NET Core.

Создание простых и оптимизированных компонентов

Для большинства компонентов Razor агрессивная оптимизация не требуется, поскольку большинство компонентов не повторяются в пользовательском интерфейсе и повторно отрисовываются не так часто. Например, маршрутизируемые компоненты с директивой @page и компоненты, используемые для отрисовки высокоуровневых элементов пользовательского интерфейса (диалоговые окна, формы и т. п.), обычно отображаются только в одном экземпляре и отрисовываются только в ответ на жест пользователя. Такие компоненты, как правило, не создают высокой нагрузки на подсистему отрисовки, что позволяет свободно использовать любые сочетания функций платформы, не беспокоясь о производительности при отрисовке.

Тем не менее в некоторых распространенных сценариях компоненты многократно повторяются, что часто приводит к ухудшению производительности пользовательского интерфейса.

  • Большие вложенные формы с сотнями отдельных элементов, такими как поля ввода или метки.
  • Сетки с сотнями строк или тысячами ячеек.
  • Точечные диаграммы с миллионами точек данных.

При моделировании каждого элемента, ячейки или точки данных в виде отдельного экземпляра компонента их количество зачастую столь велико, что затраты ресурсов на их отрисовку становятся критичными. В этом разделе собраны рекомендации по упрощению систем с такими компонентами, которые позволят сохранить скорость работы и реагирования пользовательского интерфейса.

Не создавайте тысячи экземпляров компонентов

Каждый компонент является обособленным объектом, который можно отрисовывать отдельно, независимо от его родительских и дочерних элементов. Правильный выбор способа для структурирования пользовательского интерфейса в иерархии компонентов позволяет контролировать степень детализации для отрисовки пользовательского интерфейса. От этого зависит итоговая производительность интерфейса.

Разделяя пользовательский интерфейс на большое число компонентов, вы уменьшаете объем повторной отрисовки при возникновении событий пользовательского интерфейса. В таблице с большим количеством строк, в каждой из которых присутствует кнопка, можно будет повторно отрисовывать только одну строку, используя дочерний компонент, а не всю страницу или таблицу. Но каждый компонент требует дополнительный объем памяти и времени ЦП для поддержки индивидуального состояния и жизненного цикла отрисовки.

В рамках проведенного инженерами группы разработчиков единицы продукта ASP.NET Core тестирования в приложении Blazor WebAssembly на отрисовку каждого экземпляра компонента затрачивалось около 0,06 мс. В тестовом приложении отрисовывался простой компонент, который принимал три параметра. На внутреннем уровне издержки связаны в первую очередь с извлечением из словарей состояния для каждого компонента, а также с передачей и получением параметров. Умножая это значение на число компонентов, легко заметить, что 2000 экземпляров компонентов увеличат время отрисовки на 0,12 секунд, что для пользователя означает уже заметное замедление пользовательского интерфейса.

Вы можете сделать компоненты более простыми, чтобы поддерживать большее их число. Тем не менее часто лучшим подходом будет уменьшение количества отрисовываемых компонентов. В следующих разделах мы опишем оба возможных подхода.

Дополнительные сведения об управлении памятью см. в статье "Управление памятью" в развернутых ASP.NET основных приложениях на стороне Blazor сервера.

Встраивание дочерних компонентов в родительские: Рассмотрим следующий фрагмент родительского компонента, который встроит дочерние компоненты в цикле:

<div class="chat">
    @foreach (var message in messages)
    {
        <ChatMessageDisplay Message="message" />
    }
</div>

ChatMessageDisplay.razor:

<div class="chat-message">
    <span class="author">@Message.Author</span>
    <span class="text">@Message.Text</span>
</div>

@code {
    [Parameter]
    public ChatMessage? Message { get; set; }
}

Представленный выше пример хорошо работает, пока не придется отобразить одновременно несколько тысяч сообщений. Чтобы отобразить все эти сообщения, мы можем отказаться от создания отдельных компонентов ChatMessageDisplay. Вместо этого следует встроить дочерний компонент в родительский. Описываемый ниже подход позволяет избежать затрат времени на отрисовку множества дочерних компонентов, однако при этом утрачивается возможность повторно отрисовывать разметку независимо для каждого дочернего компонента.

<div class="chat">
    @foreach (var message in messages)
    {
        <div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>
    }
</div>

Определите RenderFragments для повторного использования в коде: вы можете выделять дочерние компоненты только для повторного использования логики отрисовки. В этом случае можно создать повторно используемую логику отрисовки без реализации дополнительных компонентов. В блоке @code любого компонента определите фрагмент RenderFragment. Выполняйте отрисовку этого фрагмента из любого расположения столько раз, сколько необходимо.

@RenderWelcomeInfo

<p>Render the welcome content a second time:</p>

@RenderWelcomeInfo

@code {
    private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!</p>;
}

Чтобы сделать RenderTreeBuilder код повторно используемым для нескольких компонентов, объявите RenderFragmentpublic и static:

public static RenderFragment SayHello = @<h1>Hello!</h1>;

В приведенном выше примере SayHello можно вызвать из несвязанного компонента. Такая методика удобна для создания библиотек повторно используемых фрагментов кода разметки, которые позволяют избавиться от затрат ресурсов на отрисовку каждого компонента.

Делегаты RenderFragment могут принимать параметры. Следующий компонент передает сообщение (message) в делегат RenderFragment.

<div class="chat">
    @foreach (var message in messages)
    {
        @ChatMessageDisplay(message)
    }
</div>

@code {
    private RenderFragment<ChatMessage> ChatMessageDisplay = message =>
        @<div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>;
}

В описываемом выше подходе повторно используется логика отрисовки без дополнительных затрат ресурсов на компоненты. Но зато этот подход не позволяет независимо обновлять поддерево пользовательского интерфейса или пропускать визуализацию этого поддерева при отрисовке родительского элемента, так как мы убрали границу между компонентами. Назначение делегату RenderFragment поддерживается только в файлах компонентов Razor (.razor).

Для нестатического поля, метода или свойства, на которые не может ссылаться инициализатор поля, например TitleTemplate в следующем примере, используйте свойство вместо поля для RenderFragment:

protected RenderFragment DisplayTitle =>
    @<div>
        @TitleTemplate
    </div>;

Не принимайте слишком много параметров

Если компонент повторяется очень часто, например сотни или тысячи раз, необходимо помнить о затратах ресурсов на передачу и получение каждого его параметра.

Маловероятно, что большое число параметров само по себе ухудшит производительность, но оно может усложнить ситуацию при наличии других проблем. TableCell Для компонента, который отрисовывает 4000 раз в сетке, каждый параметр, переданный компоненту, добавляет около 15 мс к общей стоимости отрисовки. Передача десяти параметров требует около 150 мс и приводит к задержке отрисовки пользовательского интерфейса.

Чтобы уменьшить связанную с передачей параметров нагрузку, объедините несколько параметров в пользовательском классе. Например, компонент ячейки таблицы может принимать общий объект. В следующем примере значение Data будет разным для каждой ячейки, тогда как Options является общим для всех ее экземпляров.

@typeparam TItem

...

@code {
    [Parameter]
    public TItem? Data { get; set; }
    
    [Parameter]
    public GridOptions? Options { get; set; }
}

Однако помните, что объединение примитивных параметров в класс не всегда является преимуществом. Хотя он может уменьшить число параметров, он также влияет на поведение обнаружения изменений и отрисовки. Передача непримитивных параметров всегда запускает повторную отрисовку, так как Blazor не может знать, могут ли произвольные объекты быть изменены изнутри, тогда как передача примитивных параметров вызывает повторную отрисовку, только если их значения действительно изменились.

Кроме того, рассмотрим, что улучшением может быть отказ от использования компонента ячейки таблицы, как показано в предыдущем примере, и вместо этого встроить его логику в родительский компонент.

Замечание

Чтобы определить подход к повышению производительности, который дает наилучшие результаты среди множества доступных, как правило, требуется тестирование.

Дополнительные сведения о параметрах универсального типа (@typeparam) см. в следующих ресурсах.

Сделайте все каскадные параметры фиксированными

Компонент CascadingValue принимает необязательный параметр IsFixed.

  • Если параметр IsFixed имеет значение false (вариант по умолчанию), каждый получатель каскадного значения настраивает подписку для получения уведомлений об изменениях. Каждый [CascadingParameter] обойдется значительно дороже обычного [Parameter] из-за затрат на отслеживания подписки.
  • Если параметр IsFixed имеет значение true (например, <CascadingValue Value="someValue" IsFixed="true">), получатели получают начальное значение, но не настраивают подписку для получения обновлений. Каждый элемент [CascadingParameter] будет достаточно простым и не дороже, чем обычный [Parameter].

Присвоение параметру IsFixed значения true позволит увеличить производительность, если большое число других компонентов получает это каскадное значение. Соответственно, для каскадных значений следует по возможности присваивать параметру IsFixed значение true. Присваивать параметру IsFixed значение true можно в тех случаях, когда предоставленное значение не изменяется со временем.

Когда компонент передает this как каскадное значение, IsFixed также можно установить на true, поскольку this никогда не меняется в течение жизненного цикла компонента.

<CascadingValue Value="this" IsFixed="true">
    <SomeOtherComponents>
</CascadingValue>

Дополнительные сведения см. в статье Каскадные значения и параметры ASP.NET Core Blazor.

Не используйте сплаттинг атрибутов с CaptureUnmatchedValues

Компоненты могут получать "несопоставленные" значения параметров с помощью флага CaptureUnmatchedValues:

<div @attributes="OtherAttributes">...</div>

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? OtherAttributes { get; set; }
}

Такой подход позволяет передавать элементу произвольные дополнительные атрибуты. Тем не менее он сопряжен с достаточно высокими затратами ресурсов, поскольку отрисовщик будет выполнять следующие действия.

  • сопоставление всех полученных параметров с набором известных параметров для построения словаря;
  • отслеживание копий одного атрибута, которые переопределяют друг друга.

Используйте CaptureUnmatchedValues в тех случаях, когда производительность отрисовки компонентов не играет критической роли, например для редко повторяющихся компонентов. Для компонентов с большим масштабом отрисовки, таких как элементы большого списка или ячейки сетки, избегайте сплаттинга атрибутов.

Для получения дополнительной информации см. ASP.NET Core Blazor разбрызгивание атрибутов и произвольные параметры.

Реализуйте SetParametersAsync вручную

Одним из важнейших аспектов, влияющих на затраты ресурсов для отрисовки каждого компонента, является запись значений входящих параметров в свойства [Parameter]. Отрисовщик использует отражение для записи значений параметров, что может привести к снижению производительности в масштабе.

В некоторых крайних случаях следует избегать отражения и вручную реализовать собственную логику настройки параметров. Это может уместно в следующих случаях:

  • компонент отображается чрезвычайно часто (например, существуют сотни или тысячи его копий в пользовательском интерфейсе);
  • компонент принимает множество параметров;
  • вы заметили, что издержки на получение параметров заметно влияют на скорость реагирования пользовательского интерфейса.

В крайнем случае вы можете переопределить виртуальный метод SetParametersAsync компонента и реализовать собственную логику для конкретного компонента. Следующий пример намеренно создан так, чтобы избежать поиска в словаре.

@code {
    [Parameter]
    public int MessageId { get; set; }

    [Parameter]
    public string? Text { get; set; }

    [Parameter]
    public EventCallback<string> TextChanged { get; set; }

    [Parameter]
    public Theme CurrentTheme { get; set; }

    public override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            switch (parameter.Name)
            {
                case nameof(MessageId):
                    MessageId = (int)parameter.Value;
                    break;
                case nameof(Text):
                    Text = (string)parameter.Value;
                    break;
                case nameof(TextChanged):
                    TextChanged = (EventCallback<string>)parameter.Value;
                    break;
                case nameof(CurrentTheme):
                    CurrentTheme = (Theme)parameter.Value;
                    break;
                default:
                    throw new ArgumentException($"Unknown parameter: {parameter.Name}");
            }
        }

        return base.SetParametersAsync(ParameterView.Empty);
    }
}

В приведенном выше коде возврат базового класса SetParametersAsync запускает обычный метод жизненного цикла без повторного назначения параметров.

Как мы видим в приведенном выше коде, переопределение SetParametersAsync и предоставление пользовательской логики достаточно сложны и трудоемки, поэтому в общем случае мы не рекомендуем использовать этот подход. В исключительных ситуациях он может повысить производительность отрисовки на 20–25 %, но его стоит применять только в перечисленных выше сценариях.

Не активируйте события слишком часто

Некоторые события браузера возникают очень часто. Например, события onmousemove и onscroll могут возникать десятки или сотни раз в секунду. В большинстве случаев вам не нужно так часто обновлять интерфейс. Если события возникают слишком часто, это может снизить скорость реагирования пользовательского интерфейса или увеличить загрузку ЦП.

Вместо часто возникающих собственных событий рекомендуется использовать взаимодействие JS для регистрации обратного вызова, который используется реже. Например, следующий компонент отображает текущее положение мыши только один раз в каждые 500 мс.

@implements IDisposable
@inject IJSRuntime JS

<h1>@message</h1>

<div @ref="mouseMoveElement" style="border:1px dashed red;height:200px;">
    Move mouse here
</div>

@code {
    private ElementReference mouseMoveElement;
    private DotNetObjectReference<MyComponent>? selfReference;
    private string message = "Move the mouse in the box";

    [JSInvokable]
    public void HandleMouseMove(int x, int y)
    {
        message = $"Mouse move at {x}, {y}";
        StateHasChanged();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            selfReference = DotNetObjectReference.Create(this);
            var minInterval = 500;

            await JS.InvokeVoidAsync("onThrottledMouseMove", 
                mouseMoveElement, selfReference, minInterval);
        }
    }

    public void Dispose() => selfReference?.Dispose();
}

Соответствующий код JavaScript регистрирует прослушиватель DOM для событий перемещения мыши. В нашем примере прослушиватель событий использует функцию Lodash throttle, чтобы ограничить частоту вызовов:

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script>
  function onThrottledMouseMove(elem, component, interval) {
    elem.addEventListener('mousemove', _.throttle(e => {
      component.invokeMethodAsync('HandleMouseMove', e.offsetX, e.offsetY);
    }, interval));
  }
</script>

Избегайте повторной отрисовки после обработки событий без изменения состояния

Компоненты наследуются от ComponentBaseтого, от чего автоматически вызывается StateHasChanged после вызова обработчиков событий компонента. В некоторых случаях может оказаться ненужным или нежелательным вызывать повторную отрисовку после вызова обработчика событий. Например, возможно, обработчик событий не изменил состояние компонента. В таких случаях приложение может использовать интерфейс IHandleEvent для управления поведением обработки событий Blazor.

Замечание

Подход в этом разделе не приводит к исключениям ошибок. Дополнительные сведения и демонстрационный код, поддерживающий границы ошибок путем вызова ComponentBase.DispatchExceptionAsync, см. в разделе AsNonRenderingEventHandler + ErrorBoundary = непредвиденное поведение (dotnet/aspnetcore #54543).

Чтобы предотвратить повторную отрисовку для всех обработчиков событий компонента, реализуйте IHandleEvent и предоставьте задачу IHandleEvent.HandleEventAsync, которая вызывает обработчик событий без вызова StateHasChanged.

В следующем примере ни один из обработчиков событий, добавленных к компоненту, не активирует повторную отрисовку, поэтому HandleSelect не приводит к повторной отрисовке при вызове.

HandleSelect1.razor:

@page "/handle-select-1"
@using Microsoft.Extensions.Logging
@implements IHandleEvent
@inject ILogger<HandleSelect1> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleSelect">
    Select me (Avoids Rerender)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleSelect()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }

    Task IHandleEvent.HandleEventAsync(
        EventCallbackWorkItem callback, object? arg) => callback.InvokeAsync(arg);
}

Помимо глобального предотвращения повторной отрисовки после срабатывания всех обработчиков событий в компоненте, можно предотвратить повторную отрисовку после одного обработчика событий, применив указанный ниже служебный метод.

Добавьте в приложение EventUtil приведенный ниже класс Blazor. Статические действия и функции в верхней части класса EventUtil предоставляют обработчики, охватывающие несколько сочетаний аргументов и возвращаемых типов, которые Blazor использует при обработке событий.

EventUtil.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

public static class EventUtil
{
    public static Action AsNonRenderingEventHandler(Action callback)
        => new SyncReceiver(callback).Invoke;
    public static Action<TValue> AsNonRenderingEventHandler<TValue>(
            Action<TValue> callback)
        => new SyncReceiver<TValue>(callback).Invoke;
    public static Func<Task> AsNonRenderingEventHandler(Func<Task> callback)
        => new AsyncReceiver(callback).Invoke;
    public static Func<TValue, Task> AsNonRenderingEventHandler<TValue>(
            Func<TValue, Task> callback)
        => new AsyncReceiver<TValue>(callback).Invoke;

    private record SyncReceiver(Action callback) 
        : ReceiverBase { public void Invoke() => callback(); }
    private record SyncReceiver<T>(Action<T> callback) 
        : ReceiverBase { public void Invoke(T arg) => callback(arg); }
    private record AsyncReceiver(Func<Task> callback) 
        : ReceiverBase { public Task Invoke() => callback(); }
    private record AsyncReceiver<T>(Func<T, Task> callback) 
        : ReceiverBase { public Task Invoke(T arg) => callback(arg); }

    private record ReceiverBase : IHandleEvent
    {
        public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => 
            item.InvokeAsync(arg);
    }
}

Вызовите метод EventUtil.AsNonRenderingEventHandler, чтобы вызвать обработчик событий, который не запускает отрисовку при вызове.

В следующем примере :

  • При нажатии первой кнопки, которая вызывает HandleClick1, повторная отрисовка запускается.
  • При нажатии второй кнопки, которая вызывает HandleClick2, повторная отрисовка не запускается.
  • При нажатии третьей кнопки, которая вызывает HandleClick3, повторная отрисовка не запускается и используются аргументы события (MouseEventArgs).

HandleSelect2.razor:

@page "/handle-select-2"
@using Microsoft.Extensions.Logging
@inject ILogger<HandleSelect2> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleClick1">
    Select me (Rerenders)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler(HandleClick2)">
    Select me (Avoids Rerender)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(HandleClick3)">
    Select me (Avoids Rerender and uses <code>MouseEventArgs</code>)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleClick1()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler triggers a rerender.");
    }

    private void HandleClick2()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }
    
    private void HandleClick3(MouseEventArgs args)
    {
        dt = DateTime.Now;

        Logger.LogInformation(
            "This event handler doesn't trigger a rerender. " +
            "Mouse coordinates: {ScreenX}:{ScreenY}", 
            args.ScreenX, args.ScreenY);
    }
}

Помимо реализации интерфейса IHandleEvent, использование других рекомендаций, описанных в этой статье, также может помочь уменьшить количество нежелательных отрисовок после обработки событий. Например, переопределение ShouldRender в дочерних компонентах целевого компонента можно использовать для управления повторной отрисовкой.

Избегайте повторного создания делегатов для большого числа повторяющихся элементов или компонентов

Воссоздание Blazor в формате в цикле для элементов или компонентов может привести к снижению производительности.

Следующий компонент, представленный в статье об обработке событий, отвечает за отрисовку набора кнопок. Каждая кнопка назначает делегату его @onclick событию, что хорошо, если не так много кнопок для отрисовки.

EventHandlerExample5.razor:

@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}

Если использовать описанный выше подход для отрисовки большого количества кнопок, скорость отрисовки будет крайне низкой. Это ухудшит взаимодействие с пользователем. Чтобы отрисовать большое количество кнопок с обратным вызовом для событий щелчка, в приведенном ниже примере используется коллекция объектов кнопок, которые позволяют присвоить делегату @onclick для каждой кнопки значение Action. Описанный ниже подход позволяет Blazor не перестраивать все делегаты кнопок при каждой отрисовке:

LambdaEventPerformance.razor:

@page "/lambda-event-performance"

<h1>@heading</h1>

@foreach (var button in Buttons)
{
    <p>
        <button @key="button.Id" @onclick="button.Action">
            Button #@button.Id
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private List<Button> Buttons { get; set; } = new();

    protected override void OnInitialized()
    {
        for (var i = 0; i < 100; i++)
        {
            var button = new Button();

            button.Id = Guid.NewGuid().ToString();

            button.Action = (e) =>
            {
                UpdateHeading(button, e);
            };

            Buttons.Add(button);
        }
    }

    private void UpdateHeading(Button button, MouseEventArgs e)
    {
        heading = $"Selected #{button.Id} at {e.ClientX}:{e.ClientY}";
    }

    private class Button
    {
        public string? Id { get; set; }
        public Action<MouseEventArgs> Action { get; set; } = e => { };
    }
}