Blazor trên .NET 10 năm 2026 - Làm chủ Render Modes, Stream Rendering và Enhanced Navigation cho Full-stack C#
Posted on: 4/17/2026 1:10:42 AM
Table of contents
- 1. Blazor trên .NET 10 — Khi frontend không còn là lãnh địa riêng của JavaScript
- 2. Từ Blazor Server 2018 tới Blazor United 2026 — một dòng thời gian ngắn
- 3. Render Modes — Bốn chế độ render, một mô hình component
- 4. Static SSR — Linh hồn mới của Blazor United
- 5. Stream Rendering — HTML đi dần, user không còn phải nhìn spinner
- 6. Enhanced Navigation & Enhanced Form — SPA feel không cần SPA
- 7. WebAssembly AOT, Trimming và WebCIL — Kéo bundle về con số hợp lý
- 8. State Management — Persistent State và Enhanced Navigation
- 9. Authentication, Authorization và BFF cho Blazor United
- 10. JavaScript Interop — Khi bạn cần mượn vườn nhà JS
- 11. Hiệu năng và Observability cho Blazor Production
- 12. Blazor vs Vue 3.6 vs React 19 — Khi nào chọn cái nào?
- 13. Migration — Từ Razor Pages, MVC, hoặc Blazor Server cũ sang Blazor United
- 14. Checklist production — Triển khai Blazor vào hệ thống thực
- 15. Tương lai gần — Blazor sau .NET 10
- 16. Kết luận
- Nguồn tham khảo
1. Blazor trên .NET 10 — Khi frontend không còn là lãnh địa riêng của JavaScript
Trong gần hai thập kỷ, frontend hiện đại gần như được đồng nghĩa với JavaScript hoặc TypeScript. Mọi framework mà chúng ta hay nhắc tới — React, Vue, Angular, Svelte, Solid — đều chạy trên runtime JavaScript của trình duyệt, và mọi kỹ sư full-stack .NET về cơ bản phải duy trì hai codebase song song: một C# cho backend, một JS/TS cho frontend. Blazor ra đời năm 2018 với lời hứa đơn giản: viết UI bằng C# và Razor, chạy trên cả trình duyệt lẫn server, dùng chung model, chung DTO, chung validation với backend. Sau nhiều bản phát hành còn "mang tính thử nghiệm", đến .NET 8 thì mô hình Blazor United xuất hiện: một dự án Blazor duy nhất có thể trộn nhiều chế độ render khác nhau. Tới .NET 10 LTS (GA tháng 11/2025) thì bộ công cụ mới thực sự đủ chín để đưa Blazor vào sản phẩm quy mô lớn, kéo dài hỗ trợ tới 2028.
Bài viết này là một cuốn cẩm nang thực chiến cho kiến trúc sư và senior engineer đang đứng giữa hai lựa chọn: giữ SPA JavaScript truyền thống (Vue/React cộng ASP.NET Core API), hay chuyển sang Blazor full-stack C# với .NET 10. Chúng ta sẽ đi qua từng Render Mode, cơ chế Stream Rendering, Enhanced Navigation, AOT WebAssembly, state persistence, bảo mật, tối ưu hiệu năng, và cách migration từng phần dự án Razor Pages hay MVC sang Blazor mà không phải viết lại cả ứng dụng.
2. Từ Blazor Server 2018 tới Blazor United 2026 — một dòng thời gian ngắn
3. Render Modes — Bốn chế độ render, một mô hình component
Điểm khó hiểu nhất với người mới tới Blazor United chính là Render Mode. Cùng một file .razor có thể render theo bốn cách khác nhau tuỳ vào cách bạn khai báo @rendermode khi dùng component. Đây không phải trò ảo thuật — framework biên dịch component thành hai dạng artefact: một phần chạy trên server để render HTML tĩnh và giữ state, một phần có thể được gửi xuống browser để "hydrate" thành UI tương tác.
graph TB
SRC["MyPage.razor"] --> COMPILE["Razor compiler"]
COMPILE --> SSR["Static SSR HTML"]
COMPILE --> SRV["Blazor Server circuit"]
COMPILE --> WASM["WebAssembly bundle"]
SSR --> HTML["HTML ban đầu gửi về browser"]
HTML --> HYDRATE{"rendermode?"}
HYDRATE -->|None| STATIC["UI chỉ đọc, như MVC"]
HYDRATE -->|InteractiveServer| SIGNALR["SignalR circuit, server giữ state"]
HYDRATE -->|InteractiveWebAssembly| DOTNETWASM["Tải .NET WASM runtime, chạy browser"]
HYDRATE -->|InteractiveAuto| AUTO["Lần đầu Server, nền tải WASM cho lần sau"]
| Render Mode | Nơi chạy C# logic | Cần SignalR? | Download ban đầu | Use case điển hình |
|---|---|---|---|---|
| Static SSR (mặc định) | Server, mỗi request một lần | Không | Chỉ HTML | Trang marketing, landing, trang SEO, blog, danh sách sản phẩm |
| InteractiveServer | Server (circuit giữ state trong RAM) | Có (WebSocket) | ~90KB blazor.web.js | Admin dashboard nội bộ, app cần latency thấp với DB, muốn giữ code kín trên server |
| InteractiveWebAssembly | Trình duyệt (WASM) | Không | ~1.5–2MB runtime + app | App PWA, offline-first, nhiều tương tác cục bộ, game nhẹ, canvas |
| InteractiveAuto | Lần đầu: Server; tải xong WASM thì mọi visit sau chạy browser | Có lần đầu, sau không | Nhỏ lúc đầu, lớn khi prefetch xong | App công khai, có nhiều trang động, muốn trải nghiệm nhanh ngay lần đầu và scale tốt |
3.1 Khai báo rendermode ở component
@* Counter.razor *@
@page "/counter"
@rendermode InteractiveAuto
<h3>Counter</h3>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="Increment">Click me</button>
@code {
private int currentCount;
private void Increment() => currentCount++;
}Muốn component nào render tĩnh thì không cần @rendermode. Muốn một phần nhỏ của trang tĩnh trở nên tương tác, bạn bọc nó bằng component có @rendermode riêng — đây chính là điểm mạnh mix mà các framework JS phải dùng "island architecture" (Astro, Fresh) mới có.
Nguyên tắc chọn Render Mode
Mặc định đi từ Static SSR. Chỉ nâng lên Server hoặc WebAssembly khi component thực sự cần interactivity. Mỗi InteractiveServer component tăng tải SignalR, mỗi InteractiveWebAssembly kéo theo tải runtime. Tách trang SEO và trang app ra hai render boundary khác nhau giúp bundle size và TTFB tối ưu hơn hẳn.
4. Static SSR — Linh hồn mới của Blazor United
Rất nhiều người nghe đến Blazor thì nghĩ ngay đến "SPA chạy C# trong browser". Thực ra từ .NET 8, chế độ mặc định là Static SSR: component render một lần ở server, kết quả là HTML gửi về browser, không kèm runtime Blazor, không kèm SignalR. Giống hệt Razor Pages nhưng với cú pháp component tái sử dụng được.
Ý nghĩa lớn của Static SSR là:
- Không cost runtime trên client — trang tải nhẹ, Core Web Vitals tốt, tốt cho SEO.
- Tái sử dụng component — cùng component Blazor có thể dùng cho trang công khai (Static) và trang admin (InteractiveServer).
- Form handling nguyên bản HTTP —
EditFormvới[SupplyParameterFromForm]tương đương action trong MVC, chạy được khi không có JS. - Stream Rendering — HTML có thể gửi dần xuống trình duyệt thay vì chờ toàn bộ rồi flush (xem mục dưới).
@* Products.razor *@
@page "/products"
@attribute [StreamRendering]
@inject IProductRepository Repo
<h1>Sản phẩm nổi bật</h1>
@if (products is null)
{
<p>Đang tải…</p>
}
else
{
<ul>
@foreach (var p in products)
{
<li>@p.Name — @p.Price.ToString("C0")</li>
}
</ul>
}
@code {
private IReadOnlyList<Product>? products;
protected override async Task OnInitializedAsync()
{
products = await Repo.GetFeaturedAsync();
}
}Đoạn code trên không cần JS, không dùng SignalR, không dùng WASM. Khi kèm [StreamRendering], Blazor gửi HTML "Đang tải…" trước để trình duyệt bắt đầu paint, rồi khi OnInitializedAsync xong sẽ flush tiếp phần danh sách. Đây là cách .NET 10 giúp Razor Pages truyền thống có cảm giác "snappy" như SPA hiện đại.
5. Stream Rendering — HTML đi dần, user không còn phải nhìn spinner
Stream Rendering không phải khái niệm mới — React 18 có Suspense + streaming SSR, Vue 3 có <Suspense>, Nuxt có Partial Hydration. Điều thú vị là Blazor .NET 10 làm việc này ở cấp component với chỉ một attribute [StreamRendering], hoạt động cả ở Static SSR lẫn InteractiveServer lần đầu tải.
sequenceDiagram
participant B as Browser
participant S as ASP.NET Core
participant DB as Database
B->>S: GET /products
S->>B: HTML khung (header, <h1>, placeholder "Đang tải…")
Note over B: Browser paint ngay TTFB ~30ms
S->>DB: SELECT TOP 50 products
DB-->>S: rows
S->>B: HTML fragment danh sách (dạng <template blazor-ssr-stream>)
Note over B: Blazor runtime tự chèn vào đúng vị trí
S->>B: End of stream
Trong thực tế, điều đáng giá nhất là Largest Contentful Paint (LCP) cải thiện rõ rệt vì header và above-the-fold hiển thị ngay lập tức, phần dữ liệu chậm (query DB, gọi API) được chờ ở dưới. Với dự án thương mại điện tử chuyển từ Razor Pages thông thường sang Static SSR + Stream Rendering, nhiều team báo cáo giảm LCP từ 2.4s xuống khoảng 900ms mà không phải rewrite logic.
Lưu ý về Stream Rendering
Stream rendering yêu cầu server gửi Transfer-Encoding: chunked. Một số reverse proxy cũ (phiên bản NGINX chưa bật proxy_buffering off, Azure Front Door cũ) sẽ buffer toàn bộ response làm mất tác dụng. Luôn kiểm tra end-to-end qua trace thực tế, không chỉ localhost.
6. Enhanced Navigation & Enhanced Form — SPA feel không cần SPA
Khi bạn click một <a> trong trang Blazor Static SSR, Enhanced Navigation không reload toàn trang. Thay vào đó, JS rất nhỏ (~11KB) của Blazor sẽ fetch trang mới, diff DOM và chỉ patch những phần thay đổi. Người dùng thấy trải nghiệm như SPA: không flash trắng, không mất scroll position, giữ lại state của các component Interactive trên trang. Nhưng ở backend vẫn là Razor/Blazor render HTML đơn thuần — không cần viết reducer, không cần router JS, không cần state machine.
Enhanced Form hoạt động tương tự: submit form không reload cả trang, Blazor chỉ cập nhật phần body còn lại. Kết hợp với EditForm và model binding, bạn có luồng POST-REDIRECT-GET cổ điển, bền vững, lại có cảm giác mềm mại của SPA.
@* Layout with enhanced nav *>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
</Router>
<a href="/products" data-enhance-nav="true">Sản phẩm</a>Trên .NET 10, mặc định data-enhance-nav="true" không cần khai báo — chỉ cần bạn đưa trang vào layout bọc bởi <Router> là tự động kích hoạt. Muốn một link force reload toàn trang (ví dụ khi đăng xuất), dùng data-enhance-nav="false".
7. WebAssembly AOT, Trimming và WebCIL — Kéo bundle về con số hợp lý
Khởi đầu, Blazor WebAssembly bị chê bundle lớn. Một app "Hello World" cũng từng nặng 3–4 MB. .NET 10 khép lại phần lớn lời chê này nhờ ba kỹ thuật chính:
- Trimming — biên dịch chỉ giữ lại mã được dùng. IL Linker và ILC loại bỏ reflection path không cần, cắt bớt BCL.
- WebCIL — đóng gói assembly vào định dạng .webcil (một loại ELF giả) để vượt qua bộ lọc MIME của một số proxy/CDN chặn .dll; đồng thời giúp compression Brotli hiệu quả hơn.
- AOT (Ahead-of-Time) — biên dịch C# IL sang WASM thực thay vì IL interpreter. App chạy nhanh hơn 4–6 lần cho hot path, bù lại bundle tăng (2–3 MB).
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<BlazorEnableCompression>true</BlazorEnableCompression>
</PropertyGroup>Với WasmStripILAfterAOT mới trong .NET 10, toolchain xoá IL gốc sau khi sinh WASM native, giảm thêm ~20% bundle. Thực tế, bundle sau publish + Brotli cho một app nghiệp vụ cỡ trung (MudBlazor + EF client DTO) thường rơi vào 1.8–2.4MB — không còn là con số xa lạ với nhiều SPA React mega-app.
7.1 Jiterpreter — đường giữa Interpreter và AOT
Không phải app nào cũng chấp nhận AOT (tăng build time lên vài phút). Jiterpreter, bật mặc định từ .NET 9, biên dịch on-demand các opcode IL hot trong lúc app chạy. Đây là "partial JIT" ngay trong WASM runtime — người dùng gần như không cảm nhận chênh lệch so với AOT trên UI thường, mà build vẫn nhanh như IL interpreter.
8. State Management — Persistent State và Enhanced Navigation
Blazor không có Redux/Pinia làm chuẩn. Nhưng đổi lại, mọi thứ bạn cần đều là DI + component state + PersistentComponentState. Hãy xem kịch bản thực tế: user vào trang product detail, logic prerender ở server đã query DB và render sẵn HTML; khi component trở thành Interactive (Server hoặc WASM), chúng ta không muốn gọi DB/API lại lần nữa.
@inject PersistentComponentState ApplicationState
@code {
private Product? product;
private PersistingComponentStateSubscription subscription;
protected override async Task OnInitializedAsync()
{
subscription = ApplicationState.RegisterOnPersisting(PersistProduct);
if (!ApplicationState.TryTakeFromJson<Product>("product", out var fromCache) || fromCache is null)
{
product = await Repo.GetAsync(Id);
}
else
{
product = fromCache;
}
}
private Task PersistProduct()
{
ApplicationState.PersistAsJson("product", product);
return Task.CompletedTask;
}
public void Dispose() => subscription.Dispose();
}Điểm mới trong .NET 10: PersistentComponentState tiếp tục sống qua các lần Enhanced Navigation (trước đây chỉ tồn tại giữa prerender và first render của cùng một trang). Điều này biến nó thành "gần như client cache" cho các trang tĩnh.
8.1 Service-scoped state
Với InteractiveServer, mỗi circuit = một DI scope, nên bạn có thể đăng ký service với lifetime Scoped và chia sẻ state giữa các component trong cùng tab trình duyệt:
public class CartState
{
public event Action? Changed;
private readonly List<CartItem> _items = new();
public IReadOnlyList<CartItem> Items => _items;
public void Add(CartItem i) { _items.Add(i); Changed?.Invoke(); }
}
builder.Services.AddScoped<CartState>();Component subscribe đến Changed event trong OnInitialized và gọi StateHasChanged khi nhận. Đơn giản, không thư viện ngoài, không boilerplate kiểu reducer.
9. Authentication, Authorization và BFF cho Blazor United
Vấn đề hay gây đau đầu nhất khi áp dụng Blazor United là: cookie auth hoạt động với trang Static SSR nhưng không tự chuyển sang WASM; JWT thì phù hợp WASM nhưng khó gắn với Server-rendered. Đáp án 2026 là BFF pattern (Backend-for-Frontend): cookie httpOnly sống ở server, client chỉ làm việc với chính origin của mình, mọi request tới API bên thứ ba đều proxy qua BFF.
graph LR
USER["Browser"] -- cookie httpOnly --> BFF["ASP.NET Core BFF
Blazor + YARP"]
BFF -- AddIdentity --> IDP["OIDC Provider
(Keycloak, Auth0, Entra)"]
BFF -- token mgmt --> API1["Internal API A"]
BFF -- token mgmt --> API2["Internal API B"]
BFF -- SignalR --> WASM["Blazor WASM client"]
Về code, AddAuthentication().AddOpenIdConnect() kết hợp AddBff() (từ Duende hoặc hand-rolled) là đủ. Blazor component dùng AuthorizeView và [Authorize] như trong ASP.NET Core thường; nếu đang ở InteractiveWebAssembly, framework tự gọi /_configuration endpoint để lấy claims từ cookie server.
10. JavaScript Interop — Khi bạn cần mượn vườn nhà JS
Không phải thư viện nào cũng có bản .NET. Chart.js, TinyMCE, video player, OpenLayers, WebRTC… rất nhiều thứ vẫn cần JS. Blazor cho phép gọi JS từ C# và ngược lại qua IJSRuntime.
@inject IJSRuntime JS
private IJSObjectReference? _module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./Components/Chart.razor.js");
await _module.InvokeVoidAsync("renderChart", "chartCanvas", data);
}
}
public async ValueTask DisposeAsync()
{
if (_module is not null)
await _module.DisposeAsync();
}Ba pattern cần nhớ:
- ES module per component — một file
.razor.jsđi cùng component, import on-demand, tree-shaking tốt. - JSObjectReference — giữ tham chiếu đối tượng JS (ví dụ chart instance), dispose đúng lúc để tránh memory leak DOM.
- InvokeAsync với timeout — InteractiveServer có thể bị pause do mất kết nối; set
TimeSpanđể tránh hang UI mãi mãi.
11. Hiệu năng và Observability cho Blazor Production
Blazor dù xin lỗi thế nào thì cũng có "tax" riêng: Server mode phải giữ circuit trong RAM, WASM mode phải tải runtime lần đầu. Một checklist hiệu năng thực chiến cho production:
| Khu vực | Vấn đề hay gặp | Giải pháp .NET 10 |
|---|---|---|
| InteractiveServer | Circuit chiếm 300–500KB RAM mỗi tab, scale kém | Giới hạn CircuitOptions.MaxRetainedDisconnectedCircuits, dùng Scale-out SignalR backplane (Azure SignalR Service hoặc tự dựng Redis backplane nếu đã có Redis sẵn) |
| WASM initial load | Bundle lớn, thiết bị yếu nghẹt | Prerender Static SSR + InteractiveAuto, lazy-load assembly theo route |
| Grid tải 10k+ row | Blazor rerender toàn bảng, lag | QuickGrid + Virtualize, ChangeDetection qua ShouldRender() |
| Realtime streaming | Component rerender quá nhiều | @implements IHandleEvent tự chặn rerender mặc định; throttle qua System.Threading.Channels |
| Observability | Khó biết render nào chậm | OpenTelemetry Microsoft.AspNetCore.Components activity source, export OTLP; kết hợp browser trace với server trace qua traceparent |
QuickGrid + Virtualize: ví dụ
<QuickGrid Items="orders" Virtualize="true" ItemSize="48">
<PropertyColumn Property="@(o => o.Id)" />
<PropertyColumn Property="@(o => o.Customer.Name)" Title="Khách" />
<PropertyColumn Property="@(o => o.Total)" Format="C0" Sortable="true" />
</QuickGrid>Virtualize chỉ render các hàng hiển thị trên viewport, scroll ảo hoàn toàn. Đủ dùng cho bảng tới khoảng vài trăm nghìn dòng ở client (WASM), hàng triệu dòng nếu streaming từ server.
12. Blazor vs Vue 3.6 vs React 19 — Khi nào chọn cái nào?
Đây là câu hỏi thực dụng nhất. Không có câu trả lời "tốt hơn" tuyệt đối, chỉ có câu trả lời phù hợp từng ngữ cảnh.
| Tiêu chí | Blazor .NET 10 | Vue 3.6 Vapor | React 19 RSC |
|---|---|---|---|
| Ngôn ngữ | C# + Razor | TS/JS + SFC | TS/JS + JSX |
| Reuse model/validation với backend .NET | ★★★★★ — cùng assembly, DataAnnotations, FluentValidation | ★★ — cần share qua OpenAPI gen | ★★ — cần share qua OpenAPI gen |
| Bundle size app "trung bình" | Static SSR 0KB runtime — WASM 1.8–2.4MB | ~70KB core, Vapor bỏ VDOM nhẹ hơn nữa | ~140KB React + router + query |
| Hệ sinh thái UI component | MudBlazor, FluentUI, Radzen, Telerik | Element Plus, Vuetify, Naive UI, PrimeVue | MUI, Ant, Chakra, shadcn/ui |
| Thị trường tuyển dụng | Hẹp hơn, thiên về doanh nghiệp/fintech | Rộng, đặc biệt châu Á | Rộng nhất toàn cầu |
| Learning curve từ backend .NET | Gần như bằng 0 — viết C# là viết UI | Phải học TS + reactivity + ecosystem | Phải học TS + JSX + state + ecosystem |
| SSR + hydration mô hình | Static SSR + 4 Render Modes, built-in | Nuxt 4 riêng, island architecture | Next.js RSC, frontier nhưng phức tạp |
| Offline / PWA | WASM offline cực mạnh | Service Worker riêng | Service Worker riêng |
Công thức thực tế
Nếu backend là .NET, team dưới 10 người, ứng dụng nội bộ/doanh nghiệp → Blazor .NET 10. Nếu cần SEO cho trang công khai lớn + team frontend có sẵn JS/TS → Vue/Nuxt hoặc Next.js. Cố gắng "chuyển hết sang Blazor" để né JS thường không phải lựa chọn tốt nếu team đã mạnh React; đổi lại, dự án nội bộ mà đang có 2 đội Frontend-Backend thường rút gọn được 30% nhân sự khi thống nhất về Blazor.
13. Migration — Từ Razor Pages, MVC, hoặc Blazor Server cũ sang Blazor United
Một trong những điều ít được tài liệu hoá: bạn không cần rewrite. Blazor United sống chung tự nhiên với Razor Pages và MVC Controller trong cùng một process. Path /legacy/invoice vẫn là Razor Pages truyền thống, trong khi /dashboard đã là Blazor Static SSR + InteractiveAuto.
graph TB
ENTRY["Program.cs"] --> RP["MapRazorPages"]
ENTRY --> MVC["MapControllers"]
ENTRY --> BL["MapRazorComponents<App>().AddInteractiveServerRenderMode().AddInteractiveWebAssemblyRenderMode()"]
RP --> LEGACY["Trang cũ /invoice, /report (giữ nguyên)"]
MVC --> API["API /api/*"]
BL --> BLAZOR["Trang mới /dashboard, /settings (Blazor)"]
API -.shared.-> BLAZOR
Lộ trình thường dùng cho dự án mid-size:
- Tuần 1–2: Upgrade solution lên .NET 10, kích hoạt
MapRazorComponents<App>(). Giữ toàn bộ Razor Pages/MVC hiện tại. - Tuần 3–6: Chọn 1–2 trang "nóng" (admin dashboard, báo cáo động) viết lại thành Blazor component với
InteractiveServer. Dùng chung DbContext, service, validation. - Tuần 7–10: Các trang công khai (landing, list sản phẩm) chuyển sang Blazor Static SSR để dùng được Enhanced Navigation + Stream Rendering.
- Tuần 11+: Các trang có nhiều tương tác client (editor, canvas, báo cáo tương tác) chuyển sang
InteractiveAuto. Đánh giá TTI thật trên thiết bị trung bình của user thực.
Những thứ dễ bỏ quên khi migration
- Antiforgery token — Blazor Enhanced Form yêu cầu; nhớ
app.UseAntiforgery()trước pipeline component. - IHttpContextAccessor — trong Blazor không có khái niệm HttpContext xuyên suốt; chỉ có ở lần prerender đầu. Dependency nào cần HttpContext phải refactor.
- DbContext lifetime — InteractiveServer dùng scope = circuit (sống rất lâu). Nên bơm
IDbContextFactory<T>thay vìDbContexttrực tiếp. - Session state — Session ASP.NET truyền thống không hoạt động với InteractiveServer theo cách bạn tưởng. Dùng
Scoped serviceriêng hoặcPersistentComponentState.
14. Checklist production — Triển khai Blazor vào hệ thống thực
Server mode checklist
- Cấu hình
CircuitOptions.DisconnectedCircuitMaxRetainedvàMaxBufferedUnacknowledgedRenderBatchesphù hợp với mô hình tải. - Bật
HubOptions.ClientTimeoutInterval= 30s vàKeepAliveInterval= 15s để cân bằng pin/điện thoại vs reconnect. - Hỗ trợ sticky session ở load balancer (hoặc dùng Azure SignalR Service để khỏi lo sticky).
- Giám sát
Microsoft.AspNetCore.SignalRmetrics qua OpenTelemetry: connection duration, message rate, failed reconnect. - Giới hạn số circuit mở đồng thời per-IP để chặn DoS.
WebAssembly mode checklist
- CDN phục vụ tĩnh file
.wasm,.webcil,.datvới Brotli + cache-control 1 năm + hash filename. - Bật
SharedArrayBuffer(cần header COOP + COEP) nếu dùng multi-threaded WASM. - Content Security Policy: cho phép
wasm-unsafe-eval, siếtscript-src,connect-src. - Bật
Response Compressionvới Brotli ở CDN, fallback gzip cho browser cũ. - Đo Web Vitals thực ở field (RUM), không chỉ Lighthouse.
Chung — Security & Observability
- Luôn bật
app.UseAntiforgery(); kiểm tra Enhanced Form không bị tắt token do middleware thứ tự sai. - Trace distributed qua OpenTelemetry: client (Blazor WASM) → BFF → API → DB. Xuất OTLP về hệ observability hiện có (Tempo, Jaeger, Honeycomb…).
- Kiểm thử e2e với Playwright — Blazor có bộ test helper riêng từ .NET 10 (
Microsoft.AspNetCore.Components.Testing) cho unit test component. - Feature flag đổi Render Mode theo segment user (ví dụ bật WASM cho 10% user để đo hiệu năng thực) — dùng OpenFeature trên cả server và client.
15. Tương lai gần — Blazor sau .NET 10
Roadmap công khai của team ASP.NET Core cho biết giai đoạn 2026–2027 tập trung vào:
- Streaming Components trong WASM: hiện stream rendering mới mạnh ở Server-side; bản sau sẽ có pattern tương tự cho WASM client, tương đương Suspense của React.
- Partial hydration "island" style: chỉ hydrate những component đang nhìn thấy hoặc đang tương tác, giảm tải JS cho trang lớn.
- ML Compile: phối hợp với .NET AOT để sinh layout code thông minh hơn, giảm thêm kích thước WASM.
- Direct-to-HTTP từ WASM: không cần server BFF ở middle, truy cập API kiểu serverless với token do WebAuthn cấp — vẫn đang thử nghiệm.
Nếu bạn hỏi "Blazor có thay thế React/Vue không?" thì câu trả lời trung thực vẫn là không. JavaScript sẽ không biến mất. Nhưng Blazor United .NET 10 đã đủ trưởng thành để trở thành lựa chọn mặc định cho đội ngũ .NET muốn full-stack cùng một ngôn ngữ, và là phương án tiết kiệm nhân lực rất đáng cân nhắc cho các ứng dụng doanh nghiệp, admin portal, SaaS nội bộ. Thử viết 2–3 trang với Static SSR + InteractiveAuto trong tuần tới; bạn sẽ thấy nhiều định kiến cũ về Blazor không còn đúng ở 2026.
16. Kết luận
Blazor trên .NET 10 không còn là "ý tưởng thú vị nhưng chưa production-ready" như thời 2019. Bốn Render Modes — Static SSR, InteractiveServer, InteractiveWebAssembly, InteractiveAuto — cho phép chọn đúng công cụ cho từng đoạn trang, Stream Rendering và Enhanced Navigation mang lại cảm giác SPA mà vẫn giữ nguyên model HTTP đơn giản. AOT + Trimming + WebCIL đưa bundle WASM về gần mức chấp nhận được so với SPA JavaScript trung bình. Khi đặt cạnh Vue 3.6 Vapor hay React 19, Blazor không chiến thắng ở mọi tiêu chí, nhưng thắng tuyệt đối ở câu chuyện chia sẻ model, validation và logic với backend .NET — đó là giá trị kinh tế khó bỏ qua cho rất nhiều tổ chức.
Nếu bạn đang cân nhắc viết lại frontend, đừng bắt đầu bằng việc chọn framework. Hãy bắt đầu bằng việc vẽ ra biên giới hydration của ứng dụng: trang nào cần SEO, trang nào cần realtime, trang nào có thể static, trang nào thực sự cần chạy offline. Ánh xạ từng biên giới ấy sang Render Mode của Blazor .NET 10, bạn sẽ thấy kiến trúc tự nó hiện ra — gọn, rõ, và đủ để đi xa qua vòng đời LTS đến tận 2028.
Nguồn tham khảo
- Microsoft Learn — ASP.NET Core Blazor documentation (.NET 10)
- Microsoft Learn — Blazor render modes
- Microsoft Learn — Streaming rendering
- Microsoft Learn — Enhanced navigation and form handling
- Microsoft Learn — Blazor WebAssembly build tools and AOT
- .NET Blog — Blazor category
- GitHub — dotnet/aspnetcore
- MudBlazor — Blazor UI component library
- FluentUI Blazor (Microsoft)
Pinia 3 và TanStack Query 5 cho Vue 3.6 - State Management Hiện đại trong kỷ nguyên Vapor Mode 2026
Thiết kế Notification System 2026 - Fanout, Priority Queue, Idempotency và Template Engine cho triệu push/email/SMS mỗi ngày
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.