Рекомендации по повышению производительности ASP.NET Core Blazor

Примечание.

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

Внимание

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

В текущем выпуске см . версию .NET 8 этой статьи.

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

Примечание.

Примеры кода в этой статье используют типы ссылок, допускающие значение NULL (NRTs) и статический анализ состояния .NET компилятора NULL, которые поддерживаются в ASP.NET Core в .NET 6 или более поздней версии.

Оптимизация скорости отрисовки

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

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

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

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

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

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

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

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

В следующем примере средства для поиска авиарейсов используются закрытые поля для отслеживания необходимой информации для обнаружения изменений. Идентификаторы предыдущего прибывающего (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 секунд, что для пользователя означает уже заметное замедление пользовательского интерфейса.

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

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

Дочерние компоненты, встроенные в родительские

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

<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 можно было использовать в нескольких компонентах, объявите RenderFragment как public и 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; }
}

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

Примечание.

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

Дополнительные сведения о параметрах универсального типа (@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.

<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 Splatting и произвольные параметры атрибута CoreBlazor.

Реализуйте 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.

Чтобы предотвратить повторную отрисовку для всех обработчиков событий компонента, реализуйте 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);
}

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

Добавьте в приложение Blazor приведенный ниже класс EventUtil. Статические действия и функции в верхней части класса 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 => { };
    }
}

Оптимизируйте скорость взаимодействия в JavaScript

Передача вызовов между .NET и JavaScript требует дополнительных затрат по нескольким причинам.

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

Кроме того, для серверных Blazor приложений эти вызовы передаются по сети.

Не используйте чрезмерно подробные вызовы

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

private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
    foreach (var item in items)
    {
        await JS.InvokeVoidAsync("localStorage.setItem", item.Id, 
            JsonSerializer.Serialize(item));
    }
}

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

private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
    await JS.InvokeVoidAsync("storeAllInLocalStorage", items);
}

Соответствующая функция JavaScript сохраняет всю коллекцию элементов на клиенте.

function storeAllInLocalStorage(items) {
  items.forEach(item => {
    localStorage.setItem(item.id, JSON.stringify(item));
  });
}

Для приложений Blazor WebAssembly объединение вызовов взаимодействия JS в один вызов обычно позволяет значительно повысить производительность только в том случае, если компонент выполняет большое количество вызовов взаимодействия JS.

Рассмотрите возможность использования синхронных вызовов.

Вызов JavaScript из .NET

Этот раздел применяется только к клиентским компонентам.

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

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

Чтобы сделать синхронный вызов из .NET в JavaScript в клиентском компоненте, выполните приведение IJSRuntime , чтобы IJSInProcessRuntime сделать JS вызов взаимодействия:

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

При работе с IJSObjectReference компонентами ASP.NET Core 5.0 или более поздней версии можно использовать IJSInProcessObjectReference синхронно. IJSInProcessObjectReferenceIAsyncDisposable/IDisposable реализует и должен быть удален для сборки мусора, чтобы предотвратить утечку памяти, как показано в следующем примере:

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", 
            "./scripts.js");
        }
    }

    ...

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

Вызов .NET из JavaScript

Этот раздел применяется только к клиентским компонентам.

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

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

Чтобы сделать синхронный вызов из JavaScript в .NET в клиентском компоненте, используйте DotNet.invokeMethod вместо DotNet.invokeMethodAsyncнего.

Синхронные вызовы возможны, если соблюдаются следующие условия:

  • Компонент отображается только для выполнения в WebAssembly.
  • Вызываемая функция возвращает значение синхронно. Эта функция не является методом async и не возвращает объект Task .NET или Promise JavaScript.

Этот раздел применяется только к клиентским компонентам.

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

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

Чтобы сделать синхронный вызов из .NET в JavaScript в клиентском компоненте, выполните приведение IJSRuntime , чтобы IJSInProcessRuntime сделать JS вызов взаимодействия:

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

При работе с IJSObjectReference компонентами ASP.NET Core 5.0 или более поздней версии можно использовать IJSInProcessObjectReference синхронно. IJSInProcessObjectReferenceIAsyncDisposable/IDisposable реализует и должен быть удален для сборки мусора, чтобы предотвратить утечку памяти, как показано в следующем примере:

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", 
            "./scripts.js");
        }
    }

    ...

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

Рассмотрите возможность использования демаршаллированных вызовов.

Этот раздел относится только к приложениям Blazor WebAssembly.

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

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

Хотя использование IJSUnmarshalledRuntime обеспечивает наименьшие издержки по сравнению с другими подходами к взаимодействию JS, интерфейсы API JavaScript, необходимые для взаимодействия с этими интерфейсами API, в настоящее время не документированы и могут подвернуться критическим изменениям в будущих выпусках.

function jsInteropCall() {
  return BINDING.js_to_mono_obj("Hello world");
}
@inject IJSRuntime JS

@code {
    protected override void OnInitialized()
    {
        var unmarshalledJs = (IJSUnmarshalledRuntime)JS;
        var value = unmarshalledJs.InvokeUnmarshalled<string>("jsInteropCall");
    }
}

Использование взаимодействия JavaScript [JSImport]/[JSExport]

Взаимодействие JavaScript [JSImport]/[JSExport] для Blazor WebAssembly приложений обеспечивает улучшенную производительность и стабильность API взаимодействия в JS выпусках платформ до ASP.NET Core в .NET 7.

Дополнительные сведения см. в разделе "Импорт иJS экспорт JavaScriptJS" с ASP.NET CoreBlazor.

Компиляция AOT

Заранее скомпилированная компиляция (AOT) компилирует Blazor код .NET приложения непосредственно в собственный WebAssembly для прямого выполнения браузером. Приложения, скомпилированные с использованием AOT, приводят к увеличению размера приложений, которые загружаются дольше, но такие приложения обычно обеспечивают лучшую производительность во время выполнения, особенно для приложений, выполняющих ресурсоемкие задачи. Дополнительные сведения см. в статье ASP.NET Средства сборки Core Blazor WebAssembly и компиляция с заранеей компиляцией (AOT).

Уменьшайте размер скачиваемого приложения

Повторная компоновка среды выполнения

Сведения о том, как среда выполнения повторное связывание сводит к минимуму размер загрузки приложения, см. в статье ASP.NET Средства сборки Core Blazor WebAssembly и компиляция времени (AOT).

Использование System.Text.Json

Реализация взаимодействия JS в Blazor основана на System.Text.Json, высокопроизводительной библиотеке сериализации JSON с низким объемом занимаемой памяти. Использование System.Text.Json не должно приводить к увеличению размера полезных данных приложения по сравнению с добавлением одной или нескольких альтернативных библиотек JSON.

Руководство по миграции см. в статье Миграция с Newtonsoft.Json на System.Text.Json.

Обрезка промежуточного языка (IL)

Этот раздел относится только к приложениям Blazor WebAssembly.

Обрезка неиспользуемых сборок из Blazor WebAssembly приложения сокращает размер приложения, удаляя неиспользуемый код в двоичных файлах приложения. Дополнительные сведения см. в статье Настройка средства обрезки для ASP.NET Core Blazor.

Компоновка приложения Blazor WebAssembly уменьшает размер приложения за счет удаления неиспользуемого кода в двоичных файлах приложения. По умолчанию компоновщик промежуточного языка (IL) включается только при сборке в конфигурации Release. Чтобы воспользоваться этой возможностью, опубликуйте приложение для развертывания с помощью команды dotnet publishс параметром -c|--configuration, имеющим значение Release:

dotnet publish -c Release

Сборки с отложенной загрузкой

Этот раздел относится только к приложениям Blazor WebAssembly.

Загрузка сборок во время выполнения на том этапе, когда они необходимы для маршрута. Дополнительные сведения см. в статье Настройка средства обрезки для ASP.NET Core Blazor WebAssembly.

Сжатие

Этот раздел относится только к приложениям Blazor WebAssembly.

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

После развертывания приложения убедитесь в том, что приложение предоставляет сжатые файлы. В браузере откройте вкладку Сеть в средствах для разработчиков и убедитесь в том, что файлы предоставляются с Content-Encoding: br (сжатие Brotli) или Content-Encoding: gz (сжатие Gzip). Если узел не предоставляет сжатые файлы, выполните инструкции в статье Размещение и развертывание ASP.NET Core Blazor WebAssembly.

Отключение неиспользуемых функций

Этот раздел относится только к приложениям Blazor WebAssembly.

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

  • Для обеспечения правильности сведений о часовом поясе включается файл данных. Если приложению не нужна эта функция, ее можно отключить, присвоив свойству MSBuild BlazorEnableTimeZoneSupport в файле проекта приложения значение false:

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Для правильной работы таких интерфейсов API, как StringComparison.InvariantCultureIgnoreCase, включаются сведения о параметрах сортировки. Если вы уверены, что приложению не нужны сведения о параметрах сортировки, эту функцию можно отключить, присвоив свойству MSBuild BlazorWebAssemblyPreserveCollationData в файле проекта приложения значение false:

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>
    
  • По умолчанию Blazor WebAssembly содержит ресурсы глобализации, необходимые для отображения значений, таких как даты и денежные единицы, на языке пользователя и с его региональными параметрами. Если приложению не требуется локализация, можно настроить поддержку инвариантных языка и региональных параметров, основанных на языке и региональных параметрах en-US.