Najlepsze praktyki dotyczące wydajności interoperacyjności JavaScript w ASP.NET Core (interop)

Uwaga / Notatka

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.

Wywołania między .NET i JavaScript wymagają dodatkowych obciążeń, ponieważ:

  • Wywołania są asynchroniczne.
  • Parametry i wartości zwracane są serializowane w formacie JSON, aby zapewnić łatwy do zrozumienia mechanizm konwersji między typami .NET i JavaScript.

Ponadto w przypadku aplikacji Blazor po stronie serwera te wywołania są przekazywane przez sieć.

Unikaj nadmiernie szczegółowych wywołań

Ponieważ każde wywołanie wiąże się z pewnym obciążeniem, warto zmniejszyć liczbę wywołań. Rozważmy następujący kod, który przechowuje kolekcję elementów w pamięci przeglądarki localStorage:

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

W poprzednim przykładzie dla każdego elementu wykonywane jest osobne JS wywołanie interoperacyjne. Zamiast tego następujące podejście ogranicza wywołania JS do jednego wywołania:

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

Odpowiadająca mu funkcja JavaScript przechowuje całą kolekcję elementów na kliencie:

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

W przypadku aplikacji Blazor WebAssembly łączenie pojedynczych wywołań międzyoperacyjnych JS w jedno wywołanie zwykle znacznie poprawia wydajność tylko wtedy, gdy składnik wykonuje dużą liczbę wywołań międzyoperacyjnych JS.

Rozważ użycie wywołań synchronicznych

Wywoływanie JavaScript w środowisku .NET

Ta sekcja dotyczy tylko składników po stronie klienta.

JS wywołania międzyoperacyjne są asynchroniczne, niezależnie od tego, czy wywoływany kod jest synchroniczny, czy asynchroniczny. Wywołania są asynchroniczne, aby zapewnić zgodność komponentów z trybami renderowania po stronie serwera i klienta. Na serwerze wszystkie JS wywołania międzyoperacyjne muszą być asynchroniczne, ponieważ są wysyłane za pośrednictwem połączenia sieciowego.

Jeśli z całą pewnością wiesz, że twój komponent działa tylko na WebAssembly, możesz zdecydować się na synchroniczne wywołania międzyoperacyjne JS. Ma to nieco mniejsze obciążenie niż wykonywanie wywołań asynchronicznych i może spowodować zmniejszenie liczby cykli renderowania, ponieważ nie ma stanu pośredniego podczas oczekiwania na wyniki.

Aby wykonać synchroniczne wywołanie z platformy .NET do języka JavaScript w składniku po stronie klienta, rzutuj IJSRuntime na IJSInProcessRuntime, aby wywołać interfejs współpracy JS.

@inject IJSRuntime JS

...

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

Podczas pracy ze składnikami IJSObjectReference po stronie klienta platformy .NET 5 lub nowszej można użyć IJSInProcessObjectReference synchronicznie. IJSInProcessObjectReference implementuje IAsyncDisposable/IDisposable i powinien być usunięty do odzyskiwania pamięci, aby zapobiec wyciekowi pamięci, jak pokazano w poniższym przykładzie:

@inject IJSRuntime JS
@implements IDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import", 
                "./scripts.js");
            var value = module.Invoke<string>("javascriptFunctionIdentifier");
        }
    }

    ...

    void IDisposable.Dispose()
    {
        if (module is not null)
        {
            await module.Dispose();
        }
    }
}

W poprzednim przykładzie element JSDisconnectedException nie jest uwięziony podczas usuwania modułu, ponieważ w aplikacji Blazor nie ma obwodu SignalR-Blazor WebAssembly, który można utracić. Aby uzyskać więcej informacji, zobacz ASP.NET Core Blazor JavaScript interoperability (JS interop).

Wywoływanie .NET z JavaScript

Ta sekcja dotyczy tylko składników po stronie klienta.

JS wywołania międzyoperacyjne są asynchroniczne, niezależnie od tego, czy wywoływany kod jest synchroniczny, czy asynchroniczny. Wywołania są asynchroniczne, aby zapewnić zgodność komponentów z trybami renderowania po stronie serwera i klienta. Na serwerze wszystkie JS wywołania międzyoperacyjne muszą być asynchroniczne, ponieważ są wysyłane za pośrednictwem połączenia sieciowego.

Jeśli z całą pewnością wiesz, że twój komponent działa tylko na WebAssembly, możesz zdecydować się na synchroniczne wywołania międzyoperacyjne JS. Ma to nieco mniejsze obciążenie niż wykonywanie wywołań asynchronicznych i może spowodować zmniejszenie liczby cykli renderowania, ponieważ nie ma stanu pośredniego podczas oczekiwania na wyniki.

Aby wykonać wywołanie synchroniczne z języka JavaScript do platformy .NET w składniku po stronie klienta, użyj polecenia DotNet.invokeMethod zamiast DotNet.invokeMethodAsync.

Wywołania synchroniczne działają, jeśli:

  • Składnik jest renderowany tylko do uruchomienia w środowisku WebAssembly.
  • Wywołana funkcja zwraca wartość synchronicznie. Funkcja nie jest metodą async i nie zwraca typu .NET Task ani typu JavaScript Promise.

Ta sekcja dotyczy tylko składników po stronie klienta.

JS wywołania międzyoperacyjne są asynchroniczne, niezależnie od tego, czy wywoływany kod jest synchroniczny, czy asynchroniczny. Wywołania są asynchroniczne, aby zapewnić zgodność komponentów z trybami renderowania po stronie serwera i klienta. Na serwerze wszystkie JS wywołania międzyoperacyjne muszą być asynchroniczne, ponieważ są wysyłane za pośrednictwem połączenia sieciowego.

Jeśli z całą pewnością wiesz, że twój komponent działa tylko na WebAssembly, możesz zdecydować się na synchroniczne wywołania międzyoperacyjne JS. Ma to nieco mniejsze obciążenie niż wykonywanie wywołań asynchronicznych i może spowodować zmniejszenie liczby cykli renderowania, ponieważ nie ma stanu pośredniego podczas oczekiwania na wyniki.

Aby wykonać synchroniczne wywołanie z platformy .NET do języka JavaScript w składniku po stronie klienta, rzutuj IJSRuntime na IJSInProcessRuntime, aby wywołać interfejs współpracy JS.

@inject IJSRuntime JS

...

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

Podczas pracy ze składnikami IJSObjectReference po stronie klienta platformy .NET 5 lub nowszej można użyć IJSInProcessObjectReference synchronicznie. IJSInProcessObjectReference implementuje IAsyncDisposable/IDisposable i powinien być usunięty do odzyskiwania pamięci, aby zapobiec wyciekowi pamięci, jak pokazano w poniższym przykładzie:

@inject IJSRuntime JS
@implements IDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import", 
                "./scripts.js");
            var value = module.Invoke<string>("javascriptFunctionIdentifier");
        }
    }

    ...

    void IDisposable.Dispose()
    {
        if (module is not null)
        {
            await module.Dispose();
        }
    }
}

W poprzednim przykładzie element JSDisconnectedException nie jest uwięziony podczas usuwania modułu, ponieważ w aplikacji Blazor nie ma obwodu SignalR-Blazor WebAssembly, który można utracić. Aby uzyskać więcej informacji, zobacz ASP.NET Core Blazor JavaScript interoperability (JS interop).

Rozważ użycie niemarshalowanych wywołań

Ta sekcja dotyczy Blazor WebAssembly tylko aplikacji.

W przypadku uruchamiania w systemie Blazor WebAssembly można wykonać niemarshalowane wywołania z platformy .NET do języka JavaScript. Są to synchroniczne wywołania, które nie wykonują serializacji JSON argumentów ani zwracanych wartości. Wszystkie aspekty zarządzania pamięcią i tłumaczenia między reprezentacjami platformy .NET i języka JavaScript są pozostawione deweloperowi.

Ostrzeżenie

Chociaż użycie IJSUnmarshalledRuntime wiąże się z najmniejszym narzutem spośród podejść interoperacyjności JS, interfejsy API języka JavaScript wymagane do interakcji z tymi interfejsami API są obecnie nieudokumentowane i podlegają zmianom powodującym niezgodność w przyszłych wersjach.

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

Korzystanie z interoperacyjności języka JavaScript [JSImport]/[JSExport]

Współdziałanie języka JavaScript [JSImport]/[JSExport] w aplikacjach Blazor WebAssembly oferuje lepszą wydajność i stabilność niż interfejs API współdziałania JS w wersjach platformy wcześniejszych niż ASP.NET Core w .NET 7.

Aby uzyskać więcej informacji, zobacz JavaScript JSImport/JSExport interop with ASP.NET Core (Interopcja javaScript JSImport/JSExport with ASP.NET Core Blazor).