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

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.

4Render Modes
~1.8MBWASM app size sau AOT + trimming
60msServer mode round-trip trung bình
3 nămLTS support cho .NET 10 (đến 11/2028)

2. Từ Blazor Server 2018 tới Blazor United 2026 — một dòng thời gian ngắn

2018 — Blazor Server preview
Mô hình đầu tiên: UI render trên server, mỗi tương tác gửi diff qua SignalR. DX mượt nhưng phụ thuộc kết nối WebSocket.
2020 — Blazor WebAssembly GA
Runtime Mono/.NET được biên dịch sang WebAssembly, chạy hoàn toàn trong trình duyệt. App nặng (vài MB) nhưng không cần SignalR.
2022 — Blazor Hybrid (.NET MAUI)
Dùng lại component Blazor cho ứng dụng native desktop/mobile qua WebView.
2023 — .NET 8 Blazor United
Lần đầu có thể trộn Static SSR, Server, WebAssembly và Auto trong cùng một project. Khái niệm Render Mode xuất hiện, thay thế mô hình "hoặc Server hoặc WASM" thập kỷ trước.
2024 — .NET 9
Nâng cấp Reconnect UX (banner tự động hiện khi mất kết nối Server mode), Enhanced Navigation ổn định hơn, Jiterpreter giúp WASM tăng tốc hot path.
11/2025 — .NET 10 LTS GA
QuickGrid bổ sung virtual scrolling, ValidateEditContextAsync chuẩn hoá async validation, PersistentComponentState lan toả qua Enhanced Navigation, WASM bootstrap nhẹ hơn ~20% nhờ partitioning WebCIL. Hỗ trợ chính thức cho hydration tuần tự với Stream Rendering.
Q1 2026 — hệ sinh thái
MudBlazor 8, FluentUI Blazor 5, Radzen 6 bản cho .NET 10; YARP 2 phối hợp BFF Blazor gọn hơn; các team lớn (Visual Studio, Azure Portal một phần) chạy Blazor ở production quy mô triệu user/ngày.

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"]
Một component, bốn cách hydrate
Render ModeNơi chạy C# logicCần SignalR?Download ban đầuUse case điển hình
Static SSR (mặc định)Server, mỗi request một lầnKhôngChỉ HTMLTrang marketing, landing, trang SEO, blog, danh sách sản phẩm
InteractiveServerServer (circuit giữ state trong RAM)Có (WebSocket)~90KB blazor.web.jsAdmin dashboard nội bộ, app cần latency thấp với DB, muốn giữ code kín trên server
InteractiveWebAssemblyTrình duyệt (WASM)Không~1.5–2MB runtime + appApp PWA, offline-first, nhiều tương tác cục bộ, game nhẹ, canvas
InteractiveAutoLần đầu: Server; tải xong WASM thì mọi visit sau chạy browserCó lần đầu, sau khôngNhỏ lúc đầu, lớn khi prefetch xongApp 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 HTTPEditForm vớ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
Stream Rendering — HTML chảy xuống nhiều chặng trong cùng một response

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:

  1. 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.
  2. 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.
  3. 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"]
BFF giữ token ở server, client không đụng đến access token

Về code, AddAuthentication().AddOpenIdConnect() kết hợp AddBff() (từ Duende hoặc hand-rolled) là đủ. Blazor component dùng AuthorizeView[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ớ:

  1. ES module per component — một file .razor.js đi cùng component, import on-demand, tree-shaking tốt.
  2. JSObjectReference — giữ tham chiếu đối tượng JS (ví dụ chart instance), dispose đúng lúc để tránh memory leak DOM.
  3. 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ựcVấn đề hay gặpGiải pháp .NET 10
InteractiveServerCircuit chiếm 300–500KB RAM mỗi tab, scale kémGiớ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 loadBundle lớn, thiết bị yếu nghẹtPrerender Static SSR + InteractiveAuto, lazy-load assembly theo route
Grid tải 10k+ rowBlazor rerender toàn bảng, lagQuickGrid + Virtualize, ChangeDetection qua ShouldRender()
Realtime streamingComponent rerender quá nhiều@implements IHandleEvent tự chặn rerender mặc định; throttle qua System.Threading.Channels
ObservabilityKhó biết render nào chậmOpenTelemetry 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 10Vue 3.6 VaporReact 19 RSC
Ngôn ngữC# + RazorTS/JS + SFCTS/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 componentMudBlazor, FluentUI, Radzen, TelerikElement Plus, Vuetify, Naive UI, PrimeVueMUI, Ant, Chakra, shadcn/ui
Thị trường tuyển dụngHẹp hơn, thiên về doanh nghiệp/fintechRộng, đặc biệt châu ÁRộng nhất toàn cầu
Learning curve từ backend .NETGần như bằng 0 — viết C# là viết UIPhải học TS + reactivity + ecosystemPhải học TS + JSX + state + ecosystem
SSR + hydration mô hìnhStatic SSR + 4 Render Modes, built-inNuxt 4 riêng, island architectureNext.js RSC, frontier nhưng phức tạp
Offline / PWAWASM offline cực mạnhService Worker riêngService 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
Blazor United trong cùng ASP.NET Core host với MVC/Razor Pages

Lộ trình thường dùng cho dự án mid-size:

  1. 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.
  2. 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.
  3. 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.
  4. 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ì DbContext trự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 service riêng hoặc PersistentComponentState.

14. Checklist production — Triển khai Blazor vào hệ thống thực

Server mode checklist

  1. Cấu hình CircuitOptions.DisconnectedCircuitMaxRetainedMaxBufferedUnacknowledgedRenderBatches phù hợp với mô hình tải.
  2. Bật HubOptions.ClientTimeoutInterval = 30s và KeepAliveInterval = 15s để cân bằng pin/điện thoại vs reconnect.
  3. Hỗ trợ sticky session ở load balancer (hoặc dùng Azure SignalR Service để khỏi lo sticky).
  4. Giám sát Microsoft.AspNetCore.SignalR metrics qua OpenTelemetry: connection duration, message rate, failed reconnect.
  5. Giới hạn số circuit mở đồng thời per-IP để chặn DoS.

WebAssembly mode checklist

  1. CDN phục vụ tĩnh file .wasm, .webcil, .dat với Brotli + cache-control 1 năm + hash filename.
  2. Bật SharedArrayBuffer (cần header COOP + COEP) nếu dùng multi-threaded WASM.
  3. Content Security Policy: cho phép wasm-unsafe-eval, siết script-src, connect-src.
  4. Bật Response Compression với Brotli ở CDN, fallback gzip cho browser cũ.
  5. Đo Web Vitals thực ở field (RUM), không chỉ Lighthouse.

Chung — Security & Observability

  1. Luôn bật app.UseAntiforgery(); kiểm tra Enhanced Form không bị tắt token do middleware thứ tự sai.
  2. Trace distributed qua OpenTelemetry: client (Blazor WASM) → BFF → API → DB. Xuất OTLP về hệ observability hiện có (Tempo, Jaeger, Honeycomb…).
  3. 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.
  4. 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