Blazor czy JavaScript? Prędzej czy później to pytanie zadaje sobie każdy programista .NET, który ma "szybko dodać coś na froncie". Zamiast kolejnej dyskusji na opinie zrobiłem eksperyment: wziąłem jedną, tę samą aplikację i napisałem ją 2 razy. Raz w Blazorze, raz w czystym JavaScripcie. Te same funkcje, ten sam wygląd, to samo API po drugiej stronie.
A potem wszystko policzyłem: linie kodu, rozmiar paczki, czas ładowania. Wyszło 8 rund - w każdej punkt dla zwycięzcy, konkretne liczby i zero opinii wziętych z powietrza. Kilka rund poszło inaczej, niż się spodziewałem, a wynik końcowy ma gwiazdkę, o której warto wiedzieć, zanim wybierzesz technologię do swojego projektu.
Przejdziemy przez wszystkie rundy po kolei: start projektu, ilość kodu, komponenty, formularze i walidację, API i typowanie, wydajność i rozmiar, debugowanie oraz to, jak projekt wygląda po 3 miesiącach. Na koniec dostaniesz wynik, ściągę w tabeli i konkretną rekomendację: kiedy Blazor, a kiedy JavaScript.
Wersje i pomiary: tryby renderowania Blazora (Server, WebAssembly i Auto) są dostępne od .NET 8 i działają także w .NET 9 i 10. Wszystkie liczby w tym tekście to moje pomiary z tego konkretnego projektu: linie kodu liczone bez pustych linii i bez komentarzy, rozmiar paczki po dotnet publish i kompresji brotli (sprawdzony w zakładce Network), a czasy pierwszego ładowania zmierzone w DevTools z throttlingiem "Fast 4G" i pustym cache. U Ciebie liczby mogą wyjść inne, ale kierunek zwykle jest podobny.
Zasady pojedynku: 1 aplikacja, 2 technologie, 8 rund
Plan jest prosty: jedna aplikacja, dwie technologie, osiem rund. W każdej rundzie punkt dla zwycięzcy, a na końcu wynik i moja szczera rekomendacja. Żeby porównanie było uczciwe, obowiązują 4 zasady:
• Ta sama aplikacja 1:1. Funkcja w funkcję, ekran w ekran.
• Ten sam wygląd. Jeden wspólny CSS po obu stronach.
• To samo API. Jeden backend, dwa różne fronty.
• Liczby, nie opinie. Liczę kod, kilobajty i sekundy, zamiast oceniać na oko.
Aplikacja: TaskFlow. Obiektem badań jest TaskFlow - prosty menedżer zadań. Co potrafi?
• lista zadań z odhaczaniem zrobionych,
• formularz dodawania z walidacją,
• filtry (wszystkie, aktywne, zrobione) i wyszukiwarka,
• statystyki na żywo na górze ekranu,
• zapis danych przez REST API.
Mała rzecz, ale dotyka większości tego, co wraca w prawdziwych projektach: formularzy, walidacji, list, stanu i komunikacji z backendem.
Zawodnik #1: Blazor. W lewym narożniku Blazor - framework od Microsoftu, w którym interfejs piszesz w C#, w komponentach .razor zamiast w JavaScripcie. Jeden język na froncie i na backendzie, więc nie przełączasz się ciągle między dwoma światami. A od .NET 8 sam wybierasz tryb renderowania: Server, WebAssembly albo Auto. Do tego jeszcze wrócimy, bo to ważne.
Zawodnik #2: JavaScript. W prawym narożniku czysty JavaScript (vanilla JS) - celowo bez Reacta, Vue i Angulara. Dlaczego? Bo kiedy backendowiec ma "szybko dodać coś na froncie", bardzo często sięga właśnie po vanilla JS: działa wszędzie, bez budowania i bez instalacji. Porównanie Blazora z Reactem to zupełnie inna rozmowa - na osobny tekst.
Runda 1: start projektu
Pierwsza runda: od zera do działającej strony.
JavaScript: 3 pliki i działa. Tworzysz 3 pliki, otwierasz index.html w przeglądarce i... działa:
projekt/
index.html
style.css
app.jsCała logika siedzi w app.js, a w index.html wystarczy jedna linijka, żeby ją podpiąć:
<script src="app.js"></script>Zero instalacji, zero kompilacji i buildów, zero czekania. Otwierasz plik i widzisz efekt.
Blazor: 1 komenda i gotowa struktura. W Blazorze zaczynasz od komendy dotnet new blazor:
dotnet new blazor -n TaskFlow
dotnet runW zamian dostajesz gotową, sensowną strukturę projektu, z routingiem i layoutem w zestawie:
TaskFlow/
Program.cs
Components/
Pages/
Layout/
wwwroot/Pierwszy ekran masz po ok. 30 sekundach. Ale potrzebujesz zainstalowanego SDK .NET i musisz aplikację uruchomić. Dla programisty .NET to codzienność, ale kroków jest po prostu więcej.
Werdykt: punkt dla JavaScriptu (0:1). Nic nie startuje szybciej niż plik otwarty w przeglądarce.
Runda 2: ilość kodu
Druga runda: ta sama funkcja - dodawanie zadania - w obu światach.
JavaScript: ręczne odświeżanie widoku. Wersja w czystym JS wygląda tak:
form.addEventListener('submit', (e) => {
e.preventDefault();
const title = input.value.trim();
tasks.push({ id: Date.now(), title, done: false });
input.value = '';
renderTasks(); /* odśwież widok RĘCZNIE */
updateStats(); /* ...i liczniki też */
});Łapiesz event, czytasz wartość z inputa, dodajesz zadanie do tablicy... i teraz najważniejsze: widok i liczniki musisz odświeżyć ręcznie. Zapomnisz o jednym wywołaniu renderTasks() albo updateStats() - i interfejs zaczyna kłamać. W tablicy masz już nowe zadanie, a na ekranie albo w statystykach go nie ma.
Blazor: UI odświeża się sam. Ta sama funkcja w komponencie TaskList.razor:
<input @bind="newTitle" />
<button @onclick="Add">Dodaj</button>
@code {
List<TaskItem> tasks = new();
string newTitle = "";
void Add() {
tasks.Add(new TaskItem { Title = newTitle });
newTitle = "";
} /* UI odświeży się SAM */
}Bindujesz input do pola (@bind), kliknięcie wywołuje metodę (@onclick), dodajesz element do listy... i koniec. Nie ma renderTasks(), nie ma updateStats(). Zero ręcznego renderu - framework śledzi zmiany i odświeża interfejs za Ciebie.
Liczby: ile kodu na dokładnie to samo? Policzyłem linie w obu wersjach (bez pustych linii i bez komentarzy):
• cała aplikacja: JavaScript - 342 linie, Blazor - 187 linii,
• sam render listy z eventami: JavaScript - 96 linii, Blazor - 31 linii.
W całej aplikacji to ok. 45% mniej kodu w Blazorze. To moje wyniki z tego konkretnego projektu - u Ciebie liczby mogą wyjść inne, ale kierunek widać dość dobrze.
Werdykt: punkt dla Blazora (1:1). Mniej kodu to mniej miejsc, w których coś może się zepsuć.
Runda 3: komponenty
Trzecia runda: komponenty, czyli jak nie kopiować tego samego kodu 5 razy.
JavaScript: "komponent" = sklejanie stringów. W mojej wersji w czystym JS "komponent" to funkcja, która skleja stringa z HTML-em:
function taskCard(task) {
const done = task.done ? 'done' : '';
return `
<div class="card ${done}">
<span>${task.title}</span>
<button data-id="${task.id}">OK</button>
</div>`;
}Da się z tym żyć. Problem zaczyna się niżej: taki HTML to zwykły tekst, więc po każdym renderze musisz od nowa znaleźć przyciski i ręcznie podpiąć do nich eventy. Zapomnisz o tym w jednym miejscu - przycisk przestaje reagować. O bugi bardzo łatwo. Do tego tytuł zadania wpisuje użytkownik, więc jeśli wkleisz go do HTML-a bez escapowania, otwierasz sobie drogę do XSS.
Blazor: prawdziwy komponent z parametrami. W Blazorze komponent to osobny plik, np. TaskCard.razor:
<div class="card @(Item.Done ? "done" : "")">
<span>@Item.Title</span>
<button @onclick="Toggle">OK</button>
</div>
@code {
[Parameter] public TaskItem Item { get; set; }
[Parameter] public EventCallback<TaskItem> OnToggle { get; set; }
Task Toggle() => OnToggle.InvokeAsync(Item);
}Deklarujesz, czego komponent potrzebuje (parametry z atrybutem [Parameter]) i jak ma poinformować rodzica o zdarzeniu (EventCallback). A potem używasz go jak zwykłego tagu HTML:
<TaskCard Item="task" OnToggle="ZmienStatus" />Eventy? Podpięte. Odświeżanie? Automatyczne. A tekst wstawiany przez @Item.Title Razor domyślnie koduje, więc problem z XSS z wersji JS znika bez dodatkowej pracy.
Werdykt: punkt dla Blazora (2:1). Prawdziwe komponenty zamiast sklejania stringów.
Runda 4: formularze i walidacja
Czwarta runda: formularze i walidacja. To tutaj wygrywa się i przegrywa aplikacje biznesowe.
JavaScript: walidacja ręczna, czyli if, if, if. W JavaScripcie każdą regułę piszesz ręcznie:
const errors = [];
if (!title)
errors.push('Tytuł jest wymagany');
if (title.length < 3)
errors.push('Minimum 3 znaki');
if (deadline < today)
errors.push('Termin z przeszłości');
if (errors.length) {
showErrors(errors); /* ręczny render */
return;
}If, if, if. A potem osobno: wyświetlanie błędów, czyszczenie błędów, style i obsługa każdego pola. Przy 3 polach - spoko. Przy 30 robi się ciężko.
Blazor: reguły na modelu, reszta sama. W Blazorze reguły trzymasz jako atrybuty na modelu, od razu z gotowym komunikatem:
public class TaskForm
{
[Required(ErrorMessage = "Tytuł jest wymagany")]
[MinLength(3, ErrorMessage = "Minimum 3 znaki")]
public string Title { get; set; }
}A formularz wygląda tak:
<EditForm Model="form" OnValidSubmit="Save">
<DataAnnotationsValidator />
<InputText @bind-Value="form.Title" />
<ValidationMessage For="() => form.Title" />
</EditForm>EditForm i DataAnnotationsValidator robią resztę:
• komunikaty przy polach pojawiają się automatycznie,
• OnValidSubmit wywołuje się dopiero przy poprawnych danych,
• jedno źródło prawdy: reguły siedzą przy modelu, a nie rozrzucone po kodzie formularza.
Werdykt: punkt dla Blazora (3:1). Reguły w jednym miejscu i walidacja deklaratywna. Dla JavaScriptu robi się nieciekawie.
Runda 5: API i typowanie
Piąta runda: rozmowa z backendem i typowanie.
JavaScript: fetch() i literówka za milion. Pobranie zadań z API wygląda tak:
const res = await fetch('/api/tasks');
const tasks = await res.json();
console.log(tasks[0].tittle);
/* undefined <-- literówka w "title" */Fetch jest prosty - i to jego cała zaleta. Ale spójrz na ostatnią linijkę: literówka w nazwie pola, "tittle" zamiast "title". JavaScript nie powie ani słowa. Kompilatora, który by to wyłapał, po prostu nie ma. Dostaniesz undefined, które popłynie dalej - i możesz to zobaczyć dopiero na produkcji. Liczba błędów zgłoszonych przed uruchomieniem: 0.
Blazor: HttpClient + typy = tarcza. Ta sama operacja i ta sama literówka w Blazorze:
var tasks = await Http
.GetFromJsonAsync<List<TaskItem>>("/api/tasks");
var t = tasks[0].Tittle;
/* BŁĄD KOMPILACJI <-- złapane od razu */Projekt się nawet nie zbuduje. O problemie dowiadujesz się w 1 sekundę, zanim ktokolwiek go zobaczy - a nie od klienta.
Bonus: wspólne modele front + back. A teraz rzecz, którą w Blazorze lubię najbardziej. Jedna klasa może służyć i w API, i na froncie:
/* Shared/TaskItem.cs */
public class TaskItem
{
public Guid Id { get; set; }
public string Title { get; set; }
public bool Done { get; set; }
}Z tej samej klasy TaskItem korzysta backend (ASP.NET Core API) i frontend (Blazor). Zmieniasz model? Kompilator wskaże wiele miejsc do poprawienia - po obu stronach naraz. W JavaScripcie, gdzie front nic nie wie o typach z backendu, dużo łatwiej coś przeoczyć. Jedna klasa, dwa światy, zero rozjazdów.
Werdykt: punkt dla Blazora (4:1). Literówki łapie kompilator, a nie klient na produkcji. Ale spokojnie - teraz JavaScript wyciąga asa z rękawa.
Runda 6: wydajność i rozmiar
Szósta runda: kilobajty i sekundy. I tu robi się ciekawie.
Ile przeglądarka musi pobrać? Moja aplikacja w JavaScripcie waży 14 KB. Cała: HTML, CSS i JS. W Blazor WebAssembly przeglądarka przy pierwszym wejściu musi pobrać środowisko .NET - u mnie wyszło ok. 1,9 MB po kompresji. To ok. 135 razy więcej danych przy pierwszym wejściu.
Czas pierwszego ładowania. Pierwsze ładowanie na 4G, z pustym cache:
• JavaScript: 0,3 s - cała apka w 14 KB,
• Blazor Server: 0,6 s - mała paczka + połączenie SignalR,
• Blazor WebAssembly: 2,1 s - pobiera środowisko .NET.
Przy kolejnych wejściach WebAssembly startuje z cache i różnica prawie znika. Ale pierwsze wrażenie robi się tylko raz.
W praktyce znaczenie tych liczb zależy od tego, kto i jak często otwiera aplikację. Pracownik, który codziennie loguje się do firmowego panelu, zapłaci za pierwsze ładowanie raz, a potem korzysta z cache. Ktoś, kto trafia na publiczną stronę z wyszukiwarki i ma ją otworzyć po raz pierwszy, zobaczy właśnie te ponad 2 sekundy - i może nie poczekać.
Blazor ma plan B: tryby renderowania. Blazor nie jest jednak skazany na ciężkie WebAssembly. Od .NET 8 masz do wyboru 3 tryby renderowania:
• Server. Mała paczka i szybki start, ale wymaga stałego połączenia z serwerem.
• WebAssembly. Działa w pełni w przeglądarce, ale ma duże pierwsze pobranie.
• Auto. Na początku korzysta z serwera, a w tle dociąga WebAssembly. Najlepsze z obu światów, ale to też najbardziej złożony model.
Co ważne, tryb wybierasz per strona, a nawet per komponent. Całą stronę ustawiasz na Auto tak:
@page "/zadania"
@rendermode InteractiveAutoa jeśli chcesz, żeby tylko jeden komponent działał na serwerze, podajesz tryb przy jego użyciu:
<TaskList @rendermode="InteractiveServer" />Werdykt: punkt dla JavaScriptu (4:2). Mimo planu B - na publicznej stronie, gdzie liczy się każda sekunda i każdy kilobajt, lekkość wygrywa.
Runda 7: debugowanie i narzędzia
Siódma runda: czym naprawiasz, gdy coś się psuje? DevTools kontra Visual Studio.
JavaScript:
• świetne DevTools w każdej przeglądarce - duży plus,
• ale brak typów sprawia, że debugowanie to często zgadywanie,
• i kończy się na console.log co drugą linię w roli debuggera.
Blazor:
• breakpointy w C# (Visual Studio albo Rider),
• pełny IntelliSense i hot reload,
• ale debugowanie WebAssembly bywa kapryśne.
Werdykt: remis (5:3). Uczciwie? Każdy wygrywa na własnym boisku, więc punkt dostają obie strony.
Runda 8: 3 miesiące później
Ostatnia runda i moja ulubiona, bo dotyczy życia z projektem, a nie sprintu: co się dzieje z kodem po 3 miesiącach?
JavaScript po 3 miesiącach. W moim przykładzie JavaScript bez frameworka urósł do sytuacji, którą zna chyba każdy:
• app.js ma 917 linii,
• wszystko zależy od wszystkiego,
• zmiana w jednym miejscu psuje 3 inne,
• refactoring? Na własne ryzyko,
• nowa osoba w projekcie pyta: "co tu się dzieje?".
Blazor po 3 miesiącach. Ta sama aplikacja w Blazorze:
• folder Components/ to 12 małych plików,
• struktura komponentów trzyma porządek,
• kompilator pilnuje każdej zmiany,
• rename i refactoring to jeden skrót w IDE,
• nowa osoba w zespole, jeśli zna C#, ma dużo łatwiejszy start.
Skąd taka różnica? Czysty JavaScript nie narzuca żadnej struktury. Każda nowa funkcja to kolejny handler, kolejne miejsce, w którym trzeba pamiętać o ręcznym odświeżeniu widoku, i kolejna zależność od wspólnego stanu. Na początku to nie przeszkadza, ale z każdym tygodniem tych połączeń jest więcej. W Blazorze strukturę wymuszają komponenty, a kompilator przy każdej zmianie sprawdza, czy wszystko nadal do siebie pasuje.
Werdykt: punkt dla Blazora (6:3). Kod piszesz raz - a utrzymujesz latami.
Wynik końcowy: Blazor 6, JavaScript 3... z gwiazdką
Wynik końcowy: Blazor 6, JavaScript 3. Wygrał Blazor.
Ale - i to bardzo ważne "ale" - ten wynik ma gwiazdkę. To pojedynek z perspektywy programisty C#, który buduje aplikację z formularzami i logiką. Większość rund (ilość kodu, komponenty, walidacja, typowanie, utrzymanie) nagradza dokładnie to, czego taka aplikacja potrzebuje. Zmień kontekst - na przykład na publiczną stronę, na której liczą się SEO i każdy kilobajt - a wynik może się odwrócić.
Kiedy wybrać Blazor, a kiedy JavaScript?
Wybierz Blazor, gdy:
• budujesz panel administracyjny albo aplikację firmową,
• Twój zespół zna C# i .NET,
• chcesz współdzielić modele z API,
• masz dużo formularzy, tabel i logiki.
Tam Blazor ma bardzo mocną pozycję.
Wybierz JavaScript, gdy:
• robisz publiczną stronę, na której liczą się SEO i czas ładowania,
• potrzebujesz małego widgetu albo pojedynczego skryptu,
• w ogóle nie masz backendu w .NET,
• każdy kilobajt ma znaczenie.
Złota zasada. I najważniejsze zdanie całego porównania:
"Nie wybieraj technologii. Wybieraj problem, który rozwiązujesz."
Blazor i JavaScript to narzędzia, nie religie.
Blazor vs JavaScript - ściąga
Wszystkie 8 rund w jednej tabeli:
| Runda | Zwycięzca | Dlaczego |
| 1. Start projektu | JavaScript | 3 pliki, otwierasz w przeglądarce i działa; Blazor wymaga SDK .NET i uruchomienia |
| 2. Ilość kodu | Blazor | 187 zamiast 342 linii (ok. 45% mniej), UI odświeża się sam |
| 3. Komponenty | Blazor | prawdziwe komponenty z parametrami zamiast sklejania stringów |
| 4. Formularze i walidacja | Blazor | reguły jako atrybuty na modelu + EditForm zamiast if, if, if |
| 5. API i typowanie | Blazor | literówka to błąd kompilacji, a nie undefined na produkcji; wspólne modele z API |
| 6. Wydajność i rozmiar | JavaScript | 14 KB i 0,3 s kontra 1,9 MB i 2,1 s w Blazor WebAssembly |
| 7. Debugowanie i narzędzia | remis | DevTools kontra breakpointy w C#, IntelliSense i hot reload |
| 8. 3 miesiące później | Blazor | 12 małych komponentów kontra app.js na 917 linii |
| Wynik | Blazor 6:3 | z gwiazdką: z perspektywy programisty C# budującego aplikację z formularzami i logiką |
Podsumowanie
Ta sama aplikacja napisana 2 razy pokazuje różnice lepiej niż jakakolwiek dyskusja na opinie:
• JavaScript wygrywa startem i lekkością: 3 pliki bez instalacji, 14 KB i 0,3 s pierwszego ładowania.
• Blazor wygrywa kodem i utrzymaniem: ok. 45% mniej kodu, prawdziwe komponenty, walidacja na modelu, typy i wspólne modele z API.
• Tryby renderowania (Server, WebAssembly, Auto) to plan B Blazora na rozmiar paczki - i wybierasz je per strona albo per komponent.
• Wynik 6:3 dla Blazora ma gwiazdkę: na publicznej stronie, gdzie liczą się SEO i każdy kilobajt, wynik może się odwrócić.
Panel admina, aplikacja firmowa i zespół, który zna C#? Blazor. Publiczna strona, mały widget albo brak backendu w .NET? JavaScript. A przede wszystkim: wybieraj problem, który rozwiązujesz, a nie technologię.
Jeśli chcesz więcej takich konkretów, dołącz do mojej społeczności - jest nas już ponad 10 000 programistów .NET. Raz w tygodniu wysyłam jednego maila: najważniejsze newsy ze świata .NET, praktyczne porady bez lania wody i rabaty na moje kursy, których nie znajdziesz nigdzie indziej. Bez codziennego bombardowania skrzynki i zero spamu - a wypisujesz się jednym kliknięciem.
Wystarczy, że zostawisz swój adres tutaj: modestprogrammer.pl/vip. Do zobaczenia w środku :)