Bagikan melalui


praktik terbaik performa inti Blazor ASP.NET

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Penting

Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.

Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Blazor dioptimalkan untuk performa tinggi dalam sebagian besar skenario antarmuka pengguna aplikasi yang realistis. Namun, performa terbaik tergantung pada pengembang yang mengadopsi pola dan fitur yang benar.

Catatan

Contoh kode dalam artikel ini mengadopsi jenis referensi nullable (NRTs) dan .NET compiler null-state static analysis, yang didukung di ASP.NET Core di .NET 6 atau yang lebih baru.

Optimalkan kecepatan penyajian

Optimalkan kecepatan penyajian untuk meminimalkan beban kerja penyajian dan meningkatkan respons UI, yang dapat menghasilkan peningkatan sepuluh kali lipat atau lebih tinggi dalam kecepatan penyajian UI.

Hindari penyajian subtrees komponen yang tidak perlu

Anda mungkin dapat menghapus sebagian besar biaya penyajian komponen induk dengan melewati rerendering subtree komponen anak saat peristiwa terjadi. Anda hanya perlu khawatir tentang melewatkan subtree rerendering yang sangat mahal untuk dirender dan menyebabkan keterlambatan UI.

Saat runtime, komponen ada dalam hierarki. Komponen akar (komponen pertama yang dimuat) memiliki komponen anak. Pada gilirannya, anak-anak akar memiliki komponen anak mereka sendiri, dan sebagainya. Saat peristiwa terjadi, seperti pengguna yang memilih tombol, proses berikut menentukan komponen mana yang akan dirender:

  1. Peristiwa dikirim ke komponen yang merender handler peristiwa. Setelah menjalankan penanganan aktivitas, komponen dirender.
  2. Ketika komponen dirender, komponen tersebut memasok salinan nilai parameter baru ke setiap komponen turunannya.
  3. Setelah sekumpulan nilai parameter baru diterima, setiap komponen memutuskan apakah akan dirender ulang. Secara default, komponen merender jika nilai parameter mungkin telah berubah, misalnya, jika merupakan objek yang dapat diubah.

Dua langkah terakhir dari urutan sebelumnya berlanjut secara rekursif ke hierarki komponen. Dalam banyak kasus, seluruh subtree dirender. Peristiwa yang menargetkan komponen tingkat tinggi dapat menyebabkan rerendering mahal karena setiap komponen di bawah komponen tingkat tinggi harus dirender.

Untuk mencegah rendering rekursi ke dalam subtree tertentu, gunakan salah satu pendekatan berikut:

  • Pastikan bahwa parameter komponen anak adalah jenis immutable primitif, seperti string, , intbool, DateTime, dan jenis serupa lainnya. Logika bawaan untuk mendeteksi perubahan secara otomatis melewati penyajian jika nilai parameter primitif yang tidak dapat diubah tidak berubah. Jika Anda merender komponen anak dengan <Customer CustomerId="@item.CustomerId" />, di mana CustomerId adalah int jenis, maka Customer komponen tidak dirender kecuali item.CustomerId perubahan.
  • Ambil alih ShouldRender:
    • Untuk menerima nilai parameter nonprimitif, seperti jenis model kustom yang kompleks, panggilan balik peristiwa, atau RenderFragment nilai.
    • Jika penulisan komponen khusus UI yang tidak berubah setelah render awal, terlepas dari perubahan nilai parameter.

Contoh alat pencarian penerbangan maskapai berikut menggunakan bidang privat untuk melacak informasi yang diperlukan untuk mendeteksi perubahan. Pengidentifikasi penerbangan masuk sebelumnya (prevInboundFlightId) dan pengidentifikasi penerbangan keluar sebelumnya (prevOutboundFlightId) melacak informasi untuk pembaruan komponen potensial berikutnya. Jika salah satu pengidentifikasi penerbangan berubah ketika parameter komponen diatur dalam OnParametersSet, komponen dirender karena shouldRender diatur ke true. Jika shouldRender mengevaluasi ke false setelah memeriksa pengidentifikasi penerbangan, rerender yang mahal dihindari:

@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;
}

Penanganan aktivitas juga dapat diatur shouldRender ke true. Untuk sebagian besar komponen, menentukan rerendering pada tingkat penangan peristiwa individu biasanya tidak diperlukan.

Untuk informasi selengkapnya, lihat sumber daya berikut:

Virtualization

Saat merender UI dalam jumlah besar dalam perulangan, misalnya, daftar atau kisi dengan ribuan entri, kuantitas operasi penyajian yang besar dapat menyebabkan jeda dalam penyajian UI. Mengingat bahwa pengguna hanya dapat melihat sejumlah kecil elemen sekaligus tanpa menggulir, seringkali boros untuk menghabiskan elemen penyajian waktu yang saat ini tidak terlihat.

BlazorVirtualize<TItem> menyediakan komponen untuk membuat perilaku tampilan dan gulir dari daftar yang sangat besar sambil hanya merender item daftar yang berada dalam viewport gulir saat ini. Misalnya, komponen dapat merender daftar dengan 100.000 entri tetapi hanya membayar biaya penyajian 20 item yang terlihat.

Untuk informasi selengkapnya, lihat virtualisasi komponen ASP.NET CoreRazor.

Membuat komponen yang ringan dan dioptimalkan

Sebagian besar Razor komponen tidak memerlukan upaya pengoptimalan yang agresif karena sebagian besar komponen tidak mengulangi di UI dan tidak merender pada frekuensi tinggi. Misalnya, komponen yang dapat dirutekan dengan @page direktif dan komponen yang digunakan untuk merender potongan UI tingkat tinggi, seperti dialog atau formulir, kemungkinan besar hanya muncul satu per satu dan hanya dirender sebagai respons terhadap gerakan pengguna. Komponen-komponen ini biasanya tidak membuat beban kerja penyajian tinggi, sehingga Anda dapat dengan bebas menggunakan kombinasi fitur kerangka kerja apa pun tanpa banyak kekhawatiran tentang performa penyajian.

Namun, ada skenario umum di mana komponen diulang dalam skala besar dan sering mengakibatkan performa UI yang buruk:

  • Bentuk berlapis besar dengan ratusan elemen individual, seperti input atau label.
  • Kisi dengan ratusan baris atau ribuan sel.
  • Menyebarkan plot dengan jutaan titik data.

Jika memodelkan setiap elemen, sel, atau titik data sebagai instans komponen terpisah, seringkali ada begitu banyak dari mereka sehingga performa penyajiannya menjadi penting. Bagian ini memberikan saran tentang membuat komponen tersebut ringan sehingga UI tetap cepat dan responsif.

Hindari ribuan instans komponen

Setiap komponen adalah pulau terpisah yang dapat merender secara independen dari orang tua dan anak-anaknya. Dengan memilih cara membagi UI menjadi hierarki komponen, Anda mengambil kendali atas granularitas penyajian UI. Hal ini dapat mengakibatkan performa yang baik atau buruk.

Dengan membagi UI menjadi komponen terpisah, Anda dapat memiliki bagian yang lebih kecil dari render UI saat peristiwa terjadi. Dalam tabel dengan banyak baris yang memiliki tombol di setiap baris, Anda mungkin hanya dapat memiliki satu baris yang dirender dengan menggunakan komponen anak alih-alih seluruh halaman atau tabel. Namun, setiap komponen memerlukan memori tambahan dan overhead CPU untuk menangani status independen dan siklus hidup penyajiannya.

Dalam pengujian yang dilakukan oleh teknisi unit produk ASP.NET Core, overhead penyajian sekitar 0,06 ms per instans komponen terlihat di aplikasi Blazor WebAssembly . Aplikasi pengujian merender komponen sederhana yang menerima tiga parameter. Secara internal, overhead sebagian besar karena mengambil status per komponen dari kamus dan melewati dan menerima parameter. Dengan perkalian, Anda dapat melihat bahwa menambahkan 2.000 instans komponen tambahan akan menambahkan 0,12 detik ke waktu penyajian dan UI akan mulai merasa lambat bagi pengguna.

Dimungkinkan untuk membuat komponen lebih ringan sehingga Anda dapat memiliki lebih banyak dari mereka. Namun, teknik yang lebih kuat sering kali untuk menghindari memiliki begitu banyak komponen untuk dirender. Bagian berikut ini menjelaskan dua pendekatan yang dapat Anda ambil.

Untuk informasi selengkapnya tentang manajemen memori, lihat Menghosting dan menyebarkan aplikasi sisi Blazor server ASP.NET Core.

Komponen anak sebaris ke dalam induknya

Pertimbangkan bagian berikut dari komponen induk yang merender komponen turunan dalam perulangan:

<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; }
}

Contoh sebelumnya berkinerja baik jika ribuan pesan tidak ditampilkan sekaligus. Untuk menampilkan ribuan pesan sekaligus, pertimbangkan untuk tidak memperhitungkan komponen terpisahChatMessageDisplay. Sebagai gantinya, sebariskan komponen anak ke dalam induk. Pendekatan berikut menghindari overhead per komponen dari penyajian begitu banyak komponen anak dengan biaya kehilangan kemampuan untuk merender markup setiap komponen anak secara independen:

<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>
Tentukan dapat RenderFragments digunakan kembali dalam kode

Anda mungkin memperhitungkan komponen anak murni sebagai cara menggunakan kembali logika penyajian. Jika demikian, Anda dapat membuat logika penyajian yang dapat digunakan kembali tanpa menerapkan komponen tambahan. Dalam blok komponen @code apa pun, tentukan RenderFragment. Render fragmen dari lokasi mana pun sebanyak yang diperlukan:

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Untuk membuat RenderTreeBuilder kode dapat digunakan kembali di beberapa komponen, nyatakan RenderFragmentpublic dan static:

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

SayHello dalam contoh sebelumnya dapat dipanggil dari komponen yang tidak terkait. Teknik ini berguna untuk membangun pustaka cuplikan markup yang dapat digunakan kembali yang dirender tanpa overhead per komponen.

RenderFragment delegasi dapat menerima parameter. Komponen berikut meneruskan pesan (message) ke RenderFragment delegasi:

<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>;
}

Pendekatan sebelumnya menggunakan kembali logika penyajian tanpa overhead per komponen. Namun, pendekatan tidak mengizinkan refresh subtree UI secara independen, juga tidak memiliki kemampuan untuk melewati penyajian subtree UI ketika induknya merender karena tidak ada batas komponen. Penugasan ke RenderFragment delegasi hanya didukung dalam Razor file komponen (.razor), dan panggilan balik peristiwa tidak didukung.

Untuk bidang, metode, atau properti non-statis yang tidak dapat dirujuk oleh penginisialisasi bidang, seperti TitleTemplate dalam contoh berikut, gunakan properti alih-alih bidang untuk RenderFragment:

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

Jangan menerima terlalu banyak parameter

Jika komponen berulang sangat sering, misalnya, ratusan atau ribuan kali, overhead meneruskan dan menerima setiap parameter dibangun.

Jarang bahwa terlalu banyak parameter sangat membatasi performa, tetapi dapat menjadi faktor. TableCell Untuk komponen yang merender 4.000 kali dalam kisi, setiap parameter yang diteruskan ke komponen menambahkan sekitar 15 md ke total biaya penyajian. Melewati sepuluh parameter membutuhkan sekitar 150 md dan menyebabkan jeda penyajian UI.

Untuk mengurangi beban parameter, bundelkan beberapa parameter di kelas kustom. Misalnya, komponen sel tabel mungkin menerima objek umum. Dalam contoh berikut, Data berbeda untuk setiap sel, tetapi Options umum di semua instans sel:

@typeparam TItem

...

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

Namun, pertimbangkan bahwa mungkin merupakan peningkatan untuk tidak memiliki komponen sel tabel, seperti yang ditunjukkan dalam contoh sebelumnya, dan sebaliknya sebaris logikanya ke komponen induk.

Catatan

Ketika beberapa pendekatan tersedia untuk meningkatkan performa, tolok ukur pendekatan biasanya diperlukan untuk menentukan pendekatan mana yang menghasilkan hasil terbaik.

Untuk informasi selengkapnya tentang parameter jenis generik (@typeparam), lihat sumber daya berikut:

Pastikan parameter berskala diperbaiki

Komponen CascadingValue memiliki parameter opsionalIsFixed:

  • Jika IsFixed adalah false (default), setiap penerima nilai bertingkat menyiapkan langganan untuk menerima pemberitahuan perubahan. Masing-masing [CascadingParameter] secara substansial lebih mahal daripada reguler [Parameter] karena pelacakan langganan.
  • Jika IsFixed adalah true (misalnya, <CascadingValue Value="someValue" IsFixed="true">), penerima menerima nilai awal tetapi tidak menyiapkan langganan untuk menerima pembaruan. Masing-masing [CascadingParameter] ringan dan tidak lebih mahal daripada biasa [Parameter].

Pengaturan IsFixed untuk true meningkatkan performa jika ada sejumlah besar komponen lain yang menerima nilai bertingkat. Jika memungkinkan, atur IsFixed ke true pada nilai bertingkat. Anda dapat mengatur IsFixed ke saat nilai yang disediakan tidak berubah dari waktu ke true waktu.

Di mana komponen lolos this sebagai nilai bertingkat, IsFixed juga dapat diatur ke true:

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

Untuk informasi selengkapnya, lihat ASP.NET Nilai dan parameter berskala CoreBlazor.

Hindari percikan atribut dengan CaptureUnmatchedValues

Komponen dapat memilih untuk menerima nilai parameter "tidak cocok" menggunakan CaptureUnmatchedValues bendera:

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

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

Pendekatan ini memungkinkan meneruskan atribut tambahan arbitrer ke elemen . Namun, pendekatan ini mahal karena perender harus:

  • Cocokkan semua parameter yang disediakan dengan sekumpulan parameter yang diketahui untuk membangun kamus.
  • Lacak bagaimana beberapa salinan atribut yang sama saling menimpa.

Gunakan CaptureUnmatchedValues di mana performa penyajian komponen tidak penting, seperti komponen yang tidak sering diulang. Untuk komponen yang dirender dalam skala besar, seperti setiap item dalam daftar besar atau di sel kisi, coba hindari percikan atribut.

Untuk informasi selengkapnya, lihat ASP.NET Parameter splatting atribut Core Blazor dan arbitrer.

Menerapkan SetParametersAsync secara manual

Sumber overhead penyajian per komponen yang signifikan menulis nilai parameter masuk ke [Parameter] properti. Perender menggunakan refleksi untuk menulis nilai parameter, yang dapat menyebabkan performa yang buruk dalam skala besar.

Dalam beberapa kasus ekstrem, Anda mungkin ingin menghindari pantulan dan menerapkan logika pengaturan parameter Anda sendiri secara manual. Ini mungkin berlaku ketika:

  • Komponen sangat sering dirender, misalnya, ketika ada ratusan atau ribuan salinan komponen di UI.
  • Komponen menerima banyak parameter.
  • Anda menemukan bahwa overhead parameter penerimaan memiliki dampak yang dapat diamati pada responsI UI.

Dalam kasus ekstrem, Anda dapat mengambil alih metode virtual SetParametersAsync komponen dan menerapkan logika khusus komponen Anda sendiri. Contoh berikut sengaja menghindari pencarian kamus:

@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);
    }
}

Dalam kode sebelumnya, mengembalikan kelas SetParametersAsync dasar menjalankan metode siklus hidup normal tanpa menetapkan parameter lagi.

Seperti yang Anda lihat dalam kode sebelumnya, mengambil alih dan menyediakan logika kustom rumit dan melelahkan, jadi kami umumnya tidak merekomendasikan untuk mengadopsi SetParametersAsync pendekatan ini. Dalam kasus ekstrem, ini dapat meningkatkan performa penyajian sebesar 20-25%, tetapi Anda hanya boleh mempertimbangkan pendekatan ini dalam skenario ekstrem yang tercantum sebelumnya di bagian ini.

Jangan memicu peristiwa terlalu cepat

Beberapa peristiwa browser sering terjadi. Misalnya, onmousemove dan onscroll dapat menembakkan puluhan atau ratusan kali per detik. Dalam kebanyakan kasus, Anda tidak perlu sering melakukan pembaruan UI. Jika peristiwa dipicu terlalu cepat, Anda dapat membahayakan respons UI atau mengonsumsi waktu CPU yang berlebihan.

Daripada menggunakan peristiwa asli yang dengan cepat menembak, pertimbangkan penggunaan JS interop untuk mendaftarkan panggilan balik yang lebih jarang diaktifkan. Misalnya, komponen berikut menampilkan posisi mouse tetapi hanya diperbarui paling banyak sekali setiap 500 mdtk:

@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();
}

Kode JavaScript yang sesuai mendaftarkan pendengar peristiwa DOM untuk gerakan mouse. Dalam contoh ini, pendengar peristiwa menggunakan fungsi Lodash throttle untuk membatasi tingkat pemanggilan:

<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>

Hindari penyajian ulang setelah menangani peristiwa tanpa perubahan status

Secara default, komponen mewarisi dari ComponentBase, yang secara otomatis memanggil StateHasChanged setelah penanganan aktivitas komponen dipanggil. Dalam beberapa kasus, mungkin tidak perlu atau tidak diinginkan untuk memicu rerender setelah penanganan aktivitas dipanggil. Misalnya, penanganan aktivitas mungkin tidak mengubah status komponen. Dalam skenario ini, aplikasi dapat memanfaatkan IHandleEvent antarmuka untuk mengontrol perilaku Blazorpenanganan peristiwa.

Untuk mencegah rerender untuk semua penanganan aktivitas komponen, terapkan IHandleEvent dan berikan IHandleEvent.HandleEventAsync tugas yang memanggil penanganan aktivitas tanpa memanggil StateHasChanged.

Dalam contoh berikut, tidak ada penanganan aktivitas yang ditambahkan ke komponen yang memicu rerender, sehingga HandleSelect tidak menghasilkan render saat dipanggil.

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);
}

Selain mencegah rerender setelah penanganan aktivitas diaktifkan dalam komponen dengan cara global, dimungkinkan untuk mencegah rerender setelah satu penanganan aktivitas dengan menggunakan metode utilitas berikut.

Tambahkan kelas berikut EventUtil ke Blazor aplikasi. Tindakan statis dan fungsi di bagian EventUtil atas kelas menyediakan handler yang mencakup beberapa kombinasi argumen dan jenis pengembalian yang Blazor digunakan saat menangani peristiwa.

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);
    }
}

Panggil EventUtil.AsNonRenderingEventHandler untuk memanggil penanganan aktivitas yang tidak memicu render saat dipanggil.

Dalam contoh berikut:

  • Memilih tombol pertama, yang memanggil HandleClick1, memicu rerender.
  • Memilih tombol kedua, yang memanggil HandleClick2, tidak memicu rerender.
  • Memilih tombol ketiga, yang memanggil HandleClick3, tidak memicu rerender dan menggunakan argumen peristiwa (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);
    }
}

Selain menerapkan IHandleEvent antarmuka, memanfaatkan praktik terbaik lain yang dijelaskan dalam artikel ini juga dapat membantu mengurangi render yang tidak diinginkan setelah peristiwa ditangani. Misalnya, mengesampingkan ShouldRender komponen turunan dari komponen target dapat digunakan untuk mengontrol penyajian.

Hindari membuat ulang delegasi untuk banyak elemen atau komponen berulang

BlazorRekreasi ekspresi lambda delegasi untuk elemen atau komponen dalam perulangan dapat menyebabkan performa yang buruk.

Komponen berikut yang diperlihatkan dalam artikel penanganan peristiwa merender sekumpulan tombol. Setiap tombol menetapkan delegasi ke peristiwanya @onclick , yang tidak masalah jika tidak ada banyak tombol untuk dirender.

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}";
    }
}

Jika sejumlah besar tombol dirender menggunakan pendekatan sebelumnya, kecepatan penyajian berdampak buruk yang menyebabkan pengalaman pengguna yang buruk. Untuk merender sejumlah besar tombol dengan panggilan balik untuk peristiwa klik, contoh berikut menggunakan kumpulan objek tombol yang menetapkan delegasi setiap tombol @onclick ke Action. Pendekatan berikut tidak perlu Blazor membangun kembali semua delegasi tombol setiap kali tombol dirender:

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 => { };
    }
}

Optimalkan kecepatan interop JavaScript

Panggilan antara .NET dan JavaScript memerlukan overhead tambahan karena:

  • Secara default, panggilan tidak sinkron.
  • Secara default, parameter dan nilai pengembalian diserialisasikan JSon untuk menyediakan mekanisme konversi yang mudah dipahami antara jenis .NET dan JavaScript.

Selain itu untuk aplikasi sisi Blazor server, panggilan ini diteruskan di seluruh jaringan.

Hindari panggilan yang terlalu halus

Karena setiap panggilan melibatkan beberapa overhead, mungkin berharga untuk mengurangi jumlah panggilan. Pertimbangkan kode berikut, yang menyimpan kumpulan item di browser localStorage:

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

Contoh sebelumnya membuat panggilan interop terpisah JS untuk setiap item. Sebagai gantinya, pendekatan berikut mengurangi JS interop ke satu panggilan:

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

Fungsi JavaScript yang sesuai menyimpan seluruh kumpulan item pada klien:

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

Untuk Blazor WebAssembly aplikasi, menggulung panggilan interop individual JS ke dalam satu panggilan biasanya hanya meningkatkan performa secara signifikan jika komponen melakukan sejumlah JS besar panggilan interop.

Pertimbangkan penggunaan panggilan sinkron

Memanggil JavaScript dari .NET

Bagian ini hanya berlaku untuk komponen sisi klien.

Panggilan interop JS tidak sinkron secara default, terlepas dari apakah kode yang dipanggil sinkron atau asinkron. Panggilan tidak sinkron secara default untuk memastikan bahwa komponen kompatibel di seluruh mode render sisi server dan sisi klien. Di server, semua JS panggilan interop harus asinkron karena dikirim melalui koneksi jaringan.

Jika Anda tahu dengan pasti bahwa komponen Anda hanya berjalan di WebAssembly, Anda dapat memilih untuk melakukan panggilan interop sinkron JS . Ini memiliki overhead yang sedikit lebih sedikit daripada melakukan panggilan asinkron dan dapat mengakibatkan lebih sedikit siklus render karena tidak ada status menengah saat menunggu hasil.

Untuk melakukan panggilan sinkron dari .NET ke JavaScript dalam komponen sisi klien, transmisikan IJSRuntime untuk IJSInProcessRuntime melakukan JS panggilan interop:

@inject IJSRuntime JS

...

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

Saat bekerja dengan IJSObjectReference komponen sisi klien ASP.NET Core 5.0 atau yang lebih baru, Anda dapat menggunakan IJSInProcessObjectReference secara sinkron sebagai gantinya. IJSInProcessObjectReferenceIAsyncDisposable/IDisposable mengimplementasikan dan harus dibuang untuk pengumpulan sampah untuk mencegah kebocoran memori, seperti yang ditunjukkan contoh berikut:

@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();
        }
    }
}

Memanggil .NET dari JavaScript

Bagian ini hanya berlaku untuk komponen sisi klien.

Panggilan interop JS tidak sinkron secara default, terlepas dari apakah kode yang dipanggil sinkron atau asinkron. Panggilan tidak sinkron secara default untuk memastikan bahwa komponen kompatibel di seluruh mode render sisi server dan sisi klien. Di server, semua JS panggilan interop harus asinkron karena dikirim melalui koneksi jaringan.

Jika Anda tahu dengan pasti bahwa komponen Anda hanya berjalan di WebAssembly, Anda dapat memilih untuk melakukan panggilan interop sinkron JS . Ini memiliki overhead yang sedikit lebih sedikit daripada melakukan panggilan asinkron dan dapat mengakibatkan lebih sedikit siklus render karena tidak ada status menengah saat menunggu hasil.

Untuk melakukan panggilan sinkron dari JavaScript ke .NET dalam komponen sisi klien, gunakan DotNet.invokeMethod alih-alih DotNet.invokeMethodAsync.

Panggilan sinkron berfungsi jika:

  • Komponen hanya dirender untuk eksekusi di WebAssembly.
  • Fungsi yang disebut mengembalikan nilai secara sinkron. Fungsi ini bukan async metode dan tidak mengembalikan .NET Task atau JavaScript Promise.

Bagian ini hanya berlaku untuk komponen sisi klien.

Panggilan interop JS tidak sinkron secara default, terlepas dari apakah kode yang dipanggil sinkron atau asinkron. Panggilan tidak sinkron secara default untuk memastikan bahwa komponen kompatibel di seluruh mode render sisi server dan sisi klien. Di server, semua JS panggilan interop harus asinkron karena dikirim melalui koneksi jaringan.

Jika Anda tahu dengan pasti bahwa komponen Anda hanya berjalan di WebAssembly, Anda dapat memilih untuk melakukan panggilan interop sinkron JS . Ini memiliki overhead yang sedikit lebih sedikit daripada melakukan panggilan asinkron dan dapat mengakibatkan lebih sedikit siklus render karena tidak ada status menengah saat menunggu hasil.

Untuk melakukan panggilan sinkron dari .NET ke JavaScript dalam komponen sisi klien, transmisikan IJSRuntime untuk IJSInProcessRuntime melakukan JS panggilan interop:

@inject IJSRuntime JS

...

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

Saat bekerja dengan IJSObjectReference komponen sisi klien ASP.NET Core 5.0 atau yang lebih baru, Anda dapat menggunakan IJSInProcessObjectReference secara sinkron sebagai gantinya. IJSInProcessObjectReferenceIAsyncDisposable/IDisposable mengimplementasikan dan harus dibuang untuk pengumpulan sampah untuk mencegah kebocoran memori, seperti yang ditunjukkan contoh berikut:

@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();
        }
    }
}

Pertimbangkan penggunaan panggilan yang tidak terenkripsi

Bagian ini hanya berlaku untuk Blazor WebAssembly aplikasi.

Saat berjalan di Blazor WebAssembly, dimungkinkan untuk melakukan panggilan tanpa nama dari .NET ke JavaScript. Ini adalah panggilan sinkron yang tidak melakukan JSserialisasi ON argumen atau mengembalikan nilai. Semua aspek manajemen memori dan terjemahan antara representasi .NET dan JavaScript diserahkan kepada pengembang.

Peringatan

Saat menggunakan IJSUnmarshalledRuntime memiliki overhead paling sedikit dari JS pendekatan interop, API JavaScript yang diperlukan untuk berinteraksi dengan API ini saat ini tidak terdokumentasi dan tunduk pada perubahan yang melanggar dalam rilis mendatang.

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");
    }
}

Menggunakan interop JavaScript [JSImport]/[JSExport]

Interop JavaScript [JSImport]/[JSExport] untuk Blazor WebAssembly aplikasi menawarkan peningkatan performa dan stabilitas atas JS API interop dalam rilis kerangka kerja sebelum ASP.NET Core di .NET 7.

Untuk informasi selengkapnya, lihat Interop Impor/JSEkspor JavaScript JSdengan ASP.NET Core Blazor.

Kompilasi ahead-of-time (AOT)

Kompilasi ahead-of-time (AOT) mengkompilasi Blazor kode .NET aplikasi langsung ke WebAssembly asli untuk eksekusi langsung oleh browser. Aplikasi yang dikompilasi AOT menghasilkan aplikasi yang lebih besar yang membutuhkan waktu lebih lama untuk diunduh, tetapi aplikasi yang dikompilasi AOT biasanya memberikan performa runtime yang lebih baik, terutama untuk aplikasi yang menjalankan tugas intensif CPU. Untuk informasi selengkapnya, lihat ASP.NET alat build Core Blazor WebAssembly dan kompilasi ahead-of-time (AOT).

Meminimalkan ukuran unduhan aplikasi

Runtime relinking

Untuk informasi tentang cara runtime relinking meminimalkan ukuran unduhan aplikasi, lihat ASP.NET alat build Core Blazor WebAssembly dan kompilasi ahead-of-time (AOT).

Menggunakan System.Text.Json

BlazorJS Implementasi interop bergantung pada System.Text.Json, yang merupakan pustaka serialisasi ON berkinerja JStinggi dengan alokasi memori rendah. Penggunaan System.Text.Json seharusnya tidak menghasilkan ukuran payload aplikasi tambahan daripada menambahkan satu atau beberapa pustaka ON alternatif JS.

Untuk panduan migrasi, lihat Cara bermigrasi dari Newtonsoft.Json ke System.Text.Json.

Pemangkasan Bahasa Perantara (IL)

Bagian ini hanya berlaku untuk Blazor WebAssembly aplikasi.

Pemangkasan rakitan yang Blazor WebAssembly tidak digunakan dari aplikasi mengurangi ukuran aplikasi dengan menghapus kode yang tidak digunakan di biner aplikasi. Untuk informasi selengkapnya, lihat Mengonfigurasi Pemangkas untuk ASP.NET Core Blazor.

Menautkan Blazor WebAssembly aplikasi mengurangi ukuran aplikasi dengan memangkas kode yang tidak digunakan di biner aplikasi. Secara default, Intermediate Language (IL) Linker hanya diaktifkan saat membangun konfigurasi Release . Untuk mendapatkan manfaat dari ini, terbitkan aplikasi untuk penyebaran menggunakan dotnet publish perintah dengan opsi -c|--configuration diatur ke Release:

dotnet publish -c Release

Rakitan beban malas

Bagian ini hanya berlaku untuk Blazor WebAssembly aplikasi.

Muat rakitan saat runtime saat rakitan diperlukan oleh rute. Untuk informasi selengkapnya, lihat rakitan beban malas di ASP.NET Core Blazor WebAssembly.

Kompresi

Bagian ini hanya berlaku untuk Blazor WebAssembly aplikasi.

Blazor WebAssembly Saat aplikasi diterbitkan, output dikompresi secara statis selama penerbitan untuk mengurangi ukuran aplikasi dan menghapus overhead untuk kompresi runtime. Blazor bergantung pada server untuk melakukan negosiasi konten dan melayani file yang dikompresi secara statis.

Setelah aplikasi disebarkan, verifikasi bahwa aplikasi menyajikan file terkompresi. Periksa tab Jaringan di alat pengembang browser dan verifikasi bahwa file disajikan dengan Content-Encoding: br (kompresi Brotli) atau Content-Encoding: gz (kompresi Gzip). Jika host tidak melayani file terkompresi, ikuti instruksi di Host dan sebarkan ASP.NET Core Blazor WebAssembly.

Menonaktifkan fitur yang tidak digunakan

Bagian ini hanya berlaku untuk Blazor WebAssembly aplikasi.

Blazor WebAssemblyRuntime termasuk fitur .NET berikut yang dapat dinonaktifkan untuk ukuran payload yang lebih kecil:

  • File data disertakan untuk membuat informasi zona waktu benar. Jika aplikasi tidak memerlukan fitur ini, pertimbangkan untuk menonaktifkannya dengan mengatur BlazorEnableTimeZoneSupport properti MSBuild di file proyek aplikasi ke false:

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Informasi kolase disertakan untuk membuat API seperti StringComparison.InvariantCultureIgnoreCase berfungsi dengan benar. Jika Anda yakin bahwa aplikasi tidak memerlukan data kolase, pertimbangkan untuk menonaktifkannya dengan mengatur BlazorWebAssemblyPreserveCollationData properti MSBuild dalam file proyek aplikasi ke false:

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>
    
  • Secara default, Blazor WebAssembly membawa sumber daya globalisasi yang diperlukan untuk menampilkan nilai, seperti tanggal dan mata uang, dalam budaya pengguna. Jika aplikasi tidak memerlukan pelokalan, Anda dapat mengonfigurasi aplikasi untuk mendukung budaya invarian, yang didasarkan pada en-US budaya.