Wprowadzenie
Pierwszy wpis na temat Blazora na blogu pojawił się mniej więcej półtora roku temu. Po serii wpisów zostawiłem temat, ale bacznie się przyglądałem, jak Blazor się rozwija. A rozwija się bardzo fajnie. Myślę, że teraz przy okazji wydania .NET 5 warto wrócić do tematu i podsumować zmiany, które pojawiły się w ostatnim czasie.
Blazor .NET 5
Stan Blazora w czasie pisania tamtych wpisów był taki, że wersja Serwer Side (kod wykonuje się po stronie serwera z komunikacją z użyciem SignalR) była stabilna. Natomiast wersja Client Side (bazująca na WebAssembly z Mono) była w fazie rozwojowej.
W maju tego roku (2020) twórcy wypuścili stabilną wersję Blazora w wersji WebAssembly. Natomiast od premiery .NET 5, Blazor zaczął w przeglądarce korzystać z .NET 5 oraz pojawiło się kilka kolejnych nowości, przez które chciałbym przejść w tym wpisie.
Korzystanie z .NET 5 przez Blazora powoduje, że dostępne są tutaj również zmiany z samego .NET 5. W tym w szczególności zwiększona wydajność – na przykład podczas generowania i parsowania jsona.
Patrząc na zmiany, Blazor ładnie nadrabia braki w stosunku do javascriptowych konkurentów. Ostatnio zacząłem się zastanawiać, czy nie przyszedł już czas na produkcyjne wykorzystanie Blazora w jakimś pobocznym projekcie, aby zobaczyć, jak się sprawdzi poza testowym środowiskiem. Z tego co wiem, to są już firmy, które tworzą produkcyjne aplikacje w Blazorze.
Izolacja CSS
W nowym Blazor mamy możliwość izolowania CSS między poszczególnymi komponentami. Jest to jedna z tych zmian, która ułatwia codzienną pracę i pokazuje, że Blazor nadrabia dystans do bibliotek i frameworków królujących w świecie frontendu.
Aby skorzytać z CSS isolation należy utworzyć plik CSS o nazwie komponentu zakończonego rozszerzeniem .razor.css. Na przykład SurveyPrompt.razor.css. Dodatkowo Visual Studio ładnie wyświetla taki plik css pod powiązanym komponentem:

W pliku CSS możemy deklarować różne reguły. Na przykład klasę title:
.title { | |
color: green; | |
} |
W samym pliku komponentu używamy takich klas w normalny sposób (linijka numer 3 na listingu poniżej), bez jakichś dodatkowych rzeczy:
<div class="alert alert-secondary mt-4" role="alert"> | |
<span class="oi oi-pencil mr-2" aria-hidden="true"></span> | |
<strong class="title">@Title</strong> | |
<span class="text-nowrap"> | |
Please take our | |
<a target="_blank" class="font-weight-bold" href="https://go.microsoft.com/fwlink/?linkid=2137916">brief survey</a> | |
</span> | |
and tell us what you think. | |
</div> | |
@code { | |
// Demonstrates how a parent component can supply parameters | |
[Parameter] | |
public string Title { get; set; } | |
} |
Czego efektem będzie napis wyświetlony w kolorze zielonym:

Gdy sprawdzimy wygenerowany kod HTML oraz CSS, to zobaczymy, że każdy element z danego komponentu ma wygenerowany specjalny atrybut zaczynający się od „b-„. Następnie użyty jest on w wygenerowanych regułach CSS:

Dzięki czemu nie musimy się martwić tym, że jakieś reguły z różnych komponentów będą na siebie nachodziły.
Podobnie mamy również dostępną izolację javascriptu – również na poziomie komponentów. Ale tego już tutaj bardziej szczegółowo nie będę opisywał. Myślę, że w przypadku zwykłych aplikacji nie jest to aż tak istotne jak izolacja CSS. Nie po to korzystamy z Blazora, aby pisać JavaScript 😉
Jeśli jesteś zainteresowany tym tematem, to odsyłam do dokumentacji – https://docs.microsoft.com/pl-pl/aspnet/core/blazor/call-javascript-from-dotnet?view=aspnetcore-5.0#blazor-javascript-isolation-and-object-references
Wirtualizacja komponentów
Wirtualizacja jest techniką, w której staramy się renderować tylko te elementy, które są widoczne dla użytkownika, co później wpływa na wydajność działania aplikacji. Przykładowo, gdy musimy wyświetlić tabelkę z tysiącem rekordów, bo wygenerowanie wszystkiego może po pierwsze trochę czasu zająć. A po drugie: późniejsze przetwarzanie tego przez przeglądarkę również zajmuje dodatkowy czas.
W nowej wersji Blazora mamy do dyspozycji komponent Virtualize, za pomocą którego możemy właśnie zrealizować wirtualizację. Jego działanie najlepiej zobaczyć na przykładzie.
Wykorzystałem tutaj domyślny kod, który jest generowany podczas tworzenia nowego projektu Blazora WebAssembly. Po stronie serwera jedyne co zmieniłem to ilość generowanych danych w metodzie Get w WeatherForecastController (linijka 5) – zamiast 5 dałem 1000:
Po stronie widoku zostawiłem domyślną implementację (tylko fragment komponentu):
<table class="table"> | |
<thead> | |
<tr> | |
<th>Date</th> | |
<th>Temp. (C)</th> | |
<th>Temp. (F)</th> | |
<th>Summary</th> | |
</tr> | |
</thead> | |
<tbody> | |
@foreach (var forecast in forecasts) | |
{ | |
<tr> | |
<td>@forecast.Date.ToShortDateString()</td> | |
<td>@forecast.TemperatureC</td> | |
<td>@forecast.TemperatureF</td> | |
<td>@forecast.Summary</td> | |
</tr> | |
} | |
</tbody> | |
</table> |
W efekcie w przeglądarce wygenerowane zostanie 1000 elementów tr w tabelce.
Zmiana foreach z widoku powyżej na komponent Virtualize jest dość prosta. W efekcie otrzymuję coś takiego:
<table class="table"> | |
<thead> | |
<tr> | |
<th>Date</th> | |
<th>Temp. (C)</th> | |
<th>Temp. (F)</th> | |
<th>Summary</th> | |
</tr> | |
</thead> | |
<tbody> | |
<Virtualize Context="forecast" Items="@forecasts"> | |
<tr> | |
<td>@forecast.Date.ToShortDateString()</td> | |
<td>@forecast.TemperatureC</td> | |
<td>@forecast.TemperatureF</td> | |
<td>@forecast.Summary</td> | |
</tr> | |
</Virtualize> | |
</tbody> | |
</table> |
W komponencie ustawiłem dwa atrybuty. Items określa kolekcję, w której są dane, czyli to co było w foreach po słowie kluczowym in. Context natomiast określa nazwę zmiennej, w której będzie pojedynczy obiekt z kolekcji, czyli to co jest przed słowem in w foreach. Nazwę tą używamy w szablonie wewnątrz komponentu Virtualize. Gdy nie użyjemy atrybutu Context, to pojedynczy obiekt będzie domyślnie dostępny pod nazwą context.
Samej zawartości, która jest w środku komponentu Virtualize, nie zmieniałem. Jest ona taka sama, jak była wcześniej w przypadku foreach.
W efekcie w przeglądarce renderowanych jest dużo mniej elementów tr:

Jest ich trochę więcej niż ilość wyświetlana aktualnie na stronie, ale jest ich dużo mniej niż ilość danych. Komponent Virtualize ładnie aktualizuje rekordy podczas przewijania. Przez co aplikacja działa szybciej.
Wirtualizacja doładowania danych
Minusem powyższego rozwiązania jest to, że aplikacja pobiera wszystkie dane z serwera, a dopiero później optymalizuje ich wyświetlanie. Przy dużej ilości danych może się okazać, że już samo pobranie danych będzie trwało długo. Na szczęście komponent Virtualize ma wsparcie również i dla tego.
Zamiast określać atrybut Items z kolekcją obiektów, możemy ustawić atrybut ItemsProvider, gdzie określamy metodę, która ma być wykonana podczas ładowania danych. Do metody tej otrzymujemy informację o ilości obiektów, które mają zostać wyświetlone oraz początkowym indeksie. Wtedy możemy odpowiednio te dane przekazać do serwera, aby zwrócić mniejszą porcję danych. W momencie przewijania ekranu będą pobierane kolejne partie danych.
Aby nie rozbudowywać zbyt mocno przykładu, nie będę robił zmian w części serwerowej. Wykorzystam wcześniej pobrane dane i skupię się tylko na obsłudze atrybutu ItemsProvider. Ale pamiętaj, że tutaj w realnej aplikacji należy zrobic stronicowanie danych po stronie serwera!
Zmieniona tabelka w komponencie wygląda tak:
<table class="table"> | |
<thead> | |
<tr> | |
<th>Date</th> | |
<th>Temp. (C)</th> | |
<th>Temp. (F)</th> | |
<th>Summary</th> | |
</tr> | |
</thead> | |
<tbody> | |
<Virtualize Context="forecast" ItemsProvider="@LoadData"> | |
<tr> | |
<td>@forecast.Date.ToShortDateString()</td> | |
<td>@forecast.TemperatureC</td> | |
<td>@forecast.TemperatureF</td> | |
<td>@forecast.Summary</td> | |
</tr> | |
</Virtualize> | |
</tbody> | |
</table> |
Atrybut Context został taki sam, jak był wcześniej. Zamiast atrybutu Items jest ItemsProvider, w którym określiłem metodę LoadData, która wygląda tak:
private ValueTask<ItemsProviderResult<WeatherForecast>> LoadData(ItemsProviderRequest request) | |
{ | |
var number = Math.Min(request.Count, forecasts.Length - request.StartIndex); | |
var items = forecasts.Skip(request.StartIndex).Take(number); | |
return new ValueTask<ItemsProviderResult<WeatherForecast>>(new ItemsProviderResult<WeatherForecast>(items, forecasts.Length)); | |
} |
Do metody jest przekazywany jeden parametr typu ItemsProviderRequest. W nim są dwie interesujące nas właściwości: Count – liczba obiektów do zwrócenia oraz StartIndex – początkowy indeks, od którego mamy zwracać dane.
Metoda zwraca obiekt typu ItemsProviderResult, który zawiera dwa elementy: rekordy z danej strony oraz liczbę wszystkich rekordów. Jak wcześniej wspomniałem, tutaj te dane powinny być pobrane bezpośrednio z serwera!
W ten sposób w nowym Blazorze możemy cieszyć się wirtualizacją danych, czyli pobieraniem oraz renderowaniem tylko tych obiektów, które będą wyświetlone użytkownikowi. Wszystko po to, aby aplikacja działała szybciej i wydajniej.
Optymalizacja startu aplikacji
W Blazorze pojawiło się kilka innych ciekawych zmian związanych z optymalizacją startu aplikacji. Mamy możliwość prerenderowania aplikacji po stronie serwera. Dzięki czemu jesteśmy w stanie szybciej pokazać naszą aplikację użytkownikowi, bo z serwera wysyłamy już wygenerowany html, który wyświetla się od razu użytkownikowi. Oczywiście dalej aplikacja działa w WebAssembly po stronie przeglądarki, więc jest to coś zupełnie innego niż działanie Server Side.
Drugą podobną zmianą, które spowoduje, że aplikacja szybciej się ładuje, jest późne ładowanie pakietów (Lazy load assemblies). W momencie, gdy mamy rozbudowaną aplikację, na przykład składającą się z wielu modułów, nie musimy ich wszystkich ładować od raz przy starcie aplikacji. Możemy to zrobić dopiero w momencie, gdy jakiś moduł jest potrzebny. Szczególnie może być to przydatne, gdy użytkownik z racji uprawnień nie ma dostępu do jakiegoś modułu aplikacji, więc po co go w ogóle ładować.
Oba powyższe mechanizmy wymagają trochę więcej pracy, więc szczegółowo nie będę ich opisywał w tym wpisie. Zrobię to w dwóch kolejnych, gdzie zajmiemy się odpowiednio każdym z tematów.
Inne zmiany
Oczywiście w nowej wersji Blazora pojawiło się więcej zmian. Ciekawsze przedstawiłem powyżej oraz bardziej szczegółowo pokażę je w kolejnych wpisach. Po pozostałe zmiany odsyłam do wpisu poświęconego zmianom w ASP.NET Core – https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-5-release-candidate-2/
Przykład
Na githubie znajduje się demo do tego wpisu, w którym są pokazane dwa pierwsze tematy (izolacja css oraz wirtualizacja). Zachęcam do pobrania i zabawy – https://github.com/danielplawgo/Blazor.NET5
Podsumowanie
Bardzo mnie cieszy, że Blazor rozwija się bardzo dynamicznie. Gorąco mu kibicuję i liczę, że za jakiś czas przy starcie projektu nie będę musiał się zastanawiać, którego frameworka javascriptowego użyjemy, tylko od razu będziemy wybierali Blazora. Myślę, że z każdą kolejną wersją zbliżamy się do tego. 🙂
A co Ty o tym sądzisz?
Dzień dobry! Dziękuję za ten artykuł.
Mam pytanie. Czy ta wirtualizacja jest też dostępna w server- side? Czy tylko w web-assembly?
Pozdrawiam
Powinno działać, ale nie bawiłem się tym, więc nie wiem, czy czasem nie ma jakiś dodatkowych problemów.