Giới thiệu
Bạn đang phát triển một ứng dụng .NET và muốn nó chạy trên cả web, Android, iOS lẫn desktop — nhưng không muốn viết lại UI ba bốn lần? Nói thật, đây là bài toán mà gần như team nào cũng gặp ở một thời điểm nào đó. Và Blazor Hybrid trong .NET MAUI chính là câu trả lời mà Microsoft đưa ra.
Thay vì duy trì hai codebase riêng biệt — một cho web (Blazor Server/WebAssembly) và một cho native (XAML + .NET MAUI) — Blazor Hybrid cho phép bạn viết Razor component một lần, rồi chạy chúng bên trong BlazorWebView native trên mọi nền tảng. Không phải WebAssembly, không phải browser — mà là code .NET chạy trực tiếp trên thiết bị.
Nghe hấp dẫn đúng không?
Trong bài viết này, mình sẽ đi từ kiến trúc tổng quan đến triển khai thực tế — bao gồm cách thiết lập solution, chia sẻ UI qua Razor Class Library, xử lý sự khác biệt giữa các nền tảng, tối ưu hiệu suất, và những bài học thực chiến từ .NET 10. Mình cũng sẽ chia sẻ một vài "bẫy" mà mình đã từng mắc phải khi làm việc với Blazor Hybrid.
Blazor Hybrid là gì và hoạt động như thế nào?
Blazor Hybrid là mô hình trong đó các Razor component chạy trực tiếp (natively) trên thiết bị, render qua một embedded Web View thông qua local interop channel. Điều quan trọng cần hiểu: đây không phải WebAssembly và cũng không chạy trong trình duyệt.
Nhiều người nhầm lẫn điểm này — mình cũng vậy khi mới bắt đầu.
Kiến trúc hoạt động
Khi bạn chạy một ứng dụng .NET MAUI Blazor Hybrid, đây là những gì xảy ra bên dưới:
- Blazor runtime chạy như code .NET thông thường trong process native của ứng dụng
- BlazorWebView — một component của .NET MAUI — nhúng WebView2 (Windows), WKWebView (iOS/macOS), hoặc Android WebView để render HTML/CSS
- Razor component render HTML rồi gửi qua local interop channel đến WebView
- Tất cả code chạy trên một process duy nhất — không có network call, không có serialization overhead giữa server và client
Điều này có nghĩa là Blazor Hybrid có toàn quyền truy cập vào tất cả API native — camera, GPS, Bluetooth, file system — thông qua .NET MAUI, đồng thời vẫn dùng HTML/CSS cho UI. Khá tiện lợi nếu bạn hỏi mình.
Blazor Hybrid vs Native .NET MAUI — Khi nào dùng gì?
| Tiêu chí | Native .NET MAUI | Blazor Hybrid |
|---|---|---|
| UI Framework | XAML + Native controls | HTML/CSS trong WebView |
| Hiệu suất UI | Cao hơn (render native) | Tốt, nhưng qua WebView |
| Chia sẻ code với web | Chỉ business logic | Cả UI lẫn business logic |
| Phù hợp khi | Cần animation phức tạp, pixel-perfect native UX | Team có kinh nghiệm web, cần deploy cả web + mobile |
| Learning curve | Cần thành thạo XAML | Dễ hơn cho web developer |
Quy tắc đơn giản: Nếu team bạn mạnh về web/Blazor và cần ứng dụng chạy trên cả web lẫn mobile — chọn Blazor Hybrid. Nếu cần hiệu suất tối đa và UX native hoàn toàn — chọn pure .NET MAUI. Đừng overthink quá, cứ thử cả hai rồi cảm nhận.
Thiết lập Solution Architecture
.NET 10 cung cấp một solution template chính thức: .NET MAUI Blazor Hybrid and Web App. Template này tạo ra ba project — và nói thật là cấu trúc của nó khá rõ ràng, dễ hiểu:
- Razor Class Library (RCL) — chứa toàn bộ Razor component dùng chung
- .NET MAUI Blazor Hybrid App — ứng dụng native cho Android, iOS, Windows, macOS
- Blazor Web App — ứng dụng web chạy trên server hoặc WebAssembly
Tạo solution từ CLI
dotnet new maui-blazor-web -n MyHybridApp
cd MyHybridApp
Sau khi tạo, bạn sẽ có cấu trúc thư mục như sau:
MyHybridApp/
├── MyHybridApp.Maui/ # .NET MAUI Blazor Hybrid App
│ ├── MauiProgram.cs
│ ├── MainPage.xaml # Chứa BlazorWebView
│ └── wwwroot/
├── MyHybridApp.Web/ # Blazor Web App
│ ├── Program.cs
│ └── Components/
├── MyHybridApp.Shared/ # Razor Class Library (RCL)
│ ├── Pages/
│ │ ├── Home.razor
│ │ └── Counter.razor
│ ├── Layout/
│ │ └── MainLayout.razor
│ └── _Imports.razor
└── MyHybridApp.sln
Cấu hình BlazorWebView trong MAUI
File MainPage.xaml của project MAUI nhúng BlazorWebView — đây là "cầu nối" giữa thế giới native và Blazor:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyHybridApp.Maui.MainPage">
<BlazorWebView HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app"
ComponentType="{x:Type shared:Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
Đăng ký service trong MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
#endif
// Đăng ký service dùng chung
builder.Services.AddSingleton<IWeatherService, WeatherService>();
// Đăng ký service native - chỉ có trên MAUI
builder.Services.AddSingleton<ILocationService, MauiLocationService>();
return builder.Build();
}
}
Chia sẻ UI qua Razor Class Library
Đây là trái tim của kiến trúc Blazor Hybrid. Razor Class Library (RCL) chứa tất cả component UI dùng chung — cả MAUI app lẫn web app đều reference nó. Và nếu bạn thiết kế tốt phần này, cuộc sống sẽ dễ dàng hơn rất nhiều.
Tạo component trong RCL
Một component Razor điển hình trong thư mục MyHybridApp.Shared/Pages/:
@page "/weather"
@inject IWeatherService WeatherService
<h1>Dự báo thời tiết</h1>
@if (_forecasts == null)
{
<p>Đang tải...</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Ngày</th>
<th>Nhiệt độ (°C)</th>
<th>Tóm tắt</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in _forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? _forecasts;
protected override async Task OnInitializedAsync()
{
_forecasts = await WeatherService.GetForecastsAsync();
}
}
Component này hoạt động giống hệt nhau trên cả web và mobile — vì nó chỉ phụ thuộc vào interface IWeatherService, không phụ thuộc vào platform cụ thể. Đây chính là sức mạnh của dependency injection.
Xử lý Render Mode giữa Web và MAUI
Đây là một vấn đề mà mình tin rằng ai cũng sẽ gặp ít nhất một lần: MAUI app luôn chạy interactive, nhưng Blazor Web App lại cần chỉ định render mode. Nếu không xử lý, bạn sẽ nhận được exception khó chịu trên MAUI.
Giải pháp? Tạo một helper class trong RCL:
// MyHybridApp.Shared/InteractiveRenderSettings.cs
public static class InteractiveRenderSettings
{
public static IComponentRenderMode? InteractiveServer { get; set; } =
RenderMode.InteractiveServer;
public static IComponentRenderMode? InteractiveAuto { get; set; } =
RenderMode.InteractiveAuto;
public static IComponentRenderMode? InteractiveWebAssembly { get; set; } =
RenderMode.InteractiveWebAssembly;
}
Trong MauiProgram.cs, đặt tất cả về null để MAUI bỏ qua render mode:
// MauiProgram.cs — thêm trước builder.Build()
InteractiveRenderSettings.InteractiveServer = null;
InteractiveRenderSettings.InteractiveAuto = null;
InteractiveRenderSettings.InteractiveWebAssembly = null;
Và trong component, sử dụng thế này:
@rendermode InteractiveRenderSettings.InteractiveServer
Trên web, render mode hoạt động bình thường. Trên MAUI, giá trị null khiến framework bỏ qua — không lỗi, không exception. Sạch sẽ và gọn gàng.
Xử lý sự khác biệt giữa các nền tảng với Abstraction
Nói thật, đây là kỹ thuật quan trọng nhất khi xây dựng Blazor Hybrid app. Nếu bạn chỉ nhớ một thứ từ bài viết này, hãy nhớ phần này: RCL định nghĩa interface, còn mỗi platform cung cấp implementation riêng.
Bước 1: Định nghĩa interface trong RCL
// MyHybridApp.Shared/Services/IDeviceInfoService.cs
public interface IDeviceInfoService
{
string GetDeviceName();
string GetPlatform();
bool IsNativeApp { get; }
}
// MyHybridApp.Shared/Services/ILocationService.cs
public interface ILocationService
{
Task<(double Latitude, double Longitude)?> GetCurrentLocationAsync();
}
Bước 2: Implementation cho MAUI (native)
// MyHybridApp.Maui/Services/MauiDeviceInfoService.cs
public class MauiDeviceInfoService : IDeviceInfoService
{
public string GetDeviceName() => DeviceInfo.Current.Name;
public string GetPlatform() => DeviceInfo.Current.Platform.ToString();
public bool IsNativeApp => true;
}
// MyHybridApp.Maui/Services/MauiLocationService.cs
public class MauiLocationService : ILocationService
{
public async Task<(double Latitude, double Longitude)?>
GetCurrentLocationAsync()
{
try
{
var status = await Permissions
.CheckStatusAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
status = await Permissions
.RequestAsync<Permissions.LocationWhenInUse>();
}
if (status != PermissionStatus.Granted)
return null;
var location = await Geolocation.Default
.GetLocationAsync(new GeolocationRequest
{
DesiredAccuracy = GeolocationAccuracy.Medium,
Timeout = TimeSpan.FromSeconds(15)
});
if (location != null)
return (location.Latitude, location.Longitude);
return null;
}
catch (Exception)
{
return null;
}
}
}
Bước 3: Implementation cho Web
// MyHybridApp.Web/Services/WebDeviceInfoService.cs
public class WebDeviceInfoService : IDeviceInfoService
{
public string GetDeviceName() => "Web Browser";
public string GetPlatform() => "Web";
public bool IsNativeApp => false;
}
// MyHybridApp.Web/Services/WebLocationService.cs
public class WebLocationService : ILocationService
{
private readonly IJSRuntime _jsRuntime;
public WebLocationService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<(double Latitude, double Longitude)?>
GetCurrentLocationAsync()
{
try
{
var result = await _jsRuntime.InvokeAsync<LocationResult>(
"getGeolocation");
return (result.Latitude, result.Longitude);
}
catch
{
return null;
}
}
}
public record LocationResult(double Latitude, double Longitude);
Bước 4: Đăng ký DI tương ứng
// MauiProgram.cs
builder.Services.AddSingleton<IDeviceInfoService, MauiDeviceInfoService>();
builder.Services.AddSingleton<ILocationService, MauiLocationService>();
// Program.cs (Web)
builder.Services.AddScoped<IDeviceInfoService, WebDeviceInfoService>();
builder.Services.AddScoped<ILocationService, WebLocationService>();
Với cách tiếp cận này, component trong RCL chỉ cần @inject ILocationService — framework sẽ tự động inject đúng implementation tùy thuộc vào platform đang chạy. Clean và dễ test.
JavaScript Interop trong Blazor Hybrid
Blazor Hybrid vẫn hỗ trợ JavaScript interop, nhưng có vài khác biệt quan trọng so với Blazor Web mà bạn nên biết:
- Gọi JS là local process call — không có network latency
- Không bị giới hạn bởi browser security sandbox
- Toàn quyền kiểm soát lifecycle của WebView
Nghe thì có vẻ giống nhau, nhưng trải nghiệm thực tế rất khác — đặc biệt là về tốc độ.
Gọi JavaScript từ C#
// Inject IJSRuntime trong component
@inject IJSRuntime JSRuntime
@code {
private async Task ShowAlert()
{
await JSRuntime.InvokeVoidAsync("alert", "Xin chào từ Blazor Hybrid!");
}
private async Task<string> GetBrowserInfo()
{
return await JSRuntime.InvokeAsync<string>(
"eval", "navigator.userAgent");
}
}
Chặn và xử lý Web Request (Mới trong .NET 10)
.NET 10 bổ sung khả năng intercept web request trong BlazorWebView. Tính năng này cực kỳ hữu ích cho các tình huống như inject header, redirect, hoặc trả về dữ liệu local mà không cần gọi API:
// MainPage.xaml.cs
blazorWebView.UrlLoading += (sender, e) =>
{
// Chặn navigation đến URL bên ngoài
if (e.Url.Host != "0.0.0.0")
{
e.UrlLoadingStrategy = UrlLoadingStrategy.OpenInWebView;
}
};
// HybridWebView — intercept và tùy chỉnh response
hybridWebView.WebResourceRequested += (sender, e) =>
{
if (e.Uri.ToString().Contains("api/local-data"))
{
e.Handled = true;
var jsonData = JsonSerializer.Serialize(GetLocalData());
e.SetResponse(
statusCode: 200,
reasonPhrase: "OK",
contentType: "application/json",
content: new MemoryStream(
Encoding.UTF8.GetBytes(jsonData))
);
}
};
Tối ưu hiệu suất cho Blazor Hybrid
Blazor Hybrid chạy trong WebView, nên có một số bottleneck đặc thù cần chú ý. Mình đã trải qua nhiều lần "ủa sao app chậm thế" trước khi rút ra được những bài học dưới đây.
1. Lazy Loading component
Không load tất cả component ngay lúc khởi động — đây là sai lầm cơ bản nhưng rất phổ biến. Sử dụng @if hoặc dynamic component để defer:
@* Chỉ load DashboardChart khi tab được chọn *@
@if (_activeTab == "analytics")
{
<DashboardChart Data="@_chartData" />
}
@* Hoặc dùng DynamicComponent *@
<DynamicComponent Type="@_selectedComponentType"
Parameters="@_componentParams" />
2. Virtualize danh sách lớn
Nếu bạn đang render một danh sách hàng trăm (hoặc hàng nghìn) item, hãy dùng component Virtualize. Nó chỉ render các item đang hiển thị trên màn hình thôi:
<Virtualize Items="@_products" Context="product" ItemSize="80">
<div class="product-card">
<img src="@product.ImageUrl"
loading="lazy"
alt="@product.Name" />
<h3>@product.Name</h3>
<p>@product.Price.ToString("C")</p>
</div>
</Virtualize>
3. Tránh block UI thread
Đây có lẽ là lỗi phổ biến nhất mà mình thấy trong Blazor Hybrid. Code C# chạy trên main thread — nếu bạn thực hiện thao tác nặng như đọc database hay gọi API, UI sẽ đóng băng hoàn toàn:
// ❌ SAI — block UI thread
protected override void OnInitialized()
{
_data = _dbContext.Products.ToList(); // Đóng băng UI
}
// ✅ ĐÚNG — chạy async
protected override async Task OnInitializedAsync()
{
_data = await Task.Run(() =>
_dbContext.Products.ToListAsync());
}
4. Giảm kích thước asset
WebView phải tải tất cả CSS, JS, và hình ảnh — nên bạn cần tối ưu những thứ này:
- Sử dụng CSS isolation (
.razor.css) thay vì file CSS global lớn - Nén hình ảnh và dùng định dạng WebP
- Lazy-load hình ảnh với
loading="lazy" - Minify JS và CSS cho production build
5. Batch JavaScript interop call
Mỗi lần gọi InvokeAsync đều có overhead serialization. Nếu cần gọi JS nhiều lần liên tiếp, hãy gộp thành một lần duy nhất. Sự khác biệt hiệu suất đáng ngạc nhiên:
// ❌ SAI — nhiều lần interop
await JSRuntime.InvokeVoidAsync("setTitle", title);
await JSRuntime.InvokeVoidAsync("setTheme", theme);
await JSRuntime.InvokeVoidAsync("setLanguage", lang);
// ✅ ĐÚNG — gộp thành một lần
await JSRuntime.InvokeVoidAsync("applySettings",
new { title, theme, lang });
Tích hợp tính năng Native trong Blazor Hybrid
Một trong những ưu điểm lớn nhất của Blazor Hybrid so với PWA là khả năng truy cập đầy đủ các API native. Dưới đây là cách tích hợp một số tính năng mà hầu như app nào cũng cần.
Camera — Chụp ảnh từ component Razor
// MyHybridApp.Shared/Services/ICameraService.cs
public interface ICameraService
{
Task<byte[]?> CapturePhotoAsync();
}
// MyHybridApp.Maui/Services/MauiCameraService.cs
public class MauiCameraService : ICameraService
{
public async Task<byte[]?> CapturePhotoAsync()
{
try
{
var photo = await MediaPicker.Default.CapturePhotoAsync();
if (photo == null) return null;
using var stream = await photo.OpenReadAsync();
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
catch
{
return null;
}
}
}
Sử dụng trong Razor component (đơn giản đến bất ngờ):
@inject ICameraService CameraService
<button @onclick="TakePhoto">Chụp ảnh</button>
@if (_photoData != null)
{
<img src="@($"data:image/jpeg;base64,{Convert.ToBase64String(_photoData)}")"
alt="Ảnh đã chụp"
style="max-width: 300px;" />
}
@code {
private byte[]? _photoData;
private async Task TakePhoto()
{
_photoData = await CameraService.CapturePhotoAsync();
}
}
Secure Storage — Lưu dữ liệu nhạy cảm
// Interface trong RCL
public interface ISecureStorageService
{
Task SetAsync(string key, string value);
Task<string?> GetAsync(string key);
Task RemoveAsync(string key);
}
// Implementation cho MAUI
public class MauiSecureStorageService : ISecureStorageService
{
public Task SetAsync(string key, string value) =>
SecureStorage.Default.SetAsync(key, value);
public Task<string?> GetAsync(string key) =>
SecureStorage.Default.GetAsync(key);
public Task RemoveAsync(string key)
{
SecureStorage.Default.Remove(key);
return Task.CompletedTask;
}
}
// Implementation cho Web — dùng ProtectedLocalStorage
public class WebSecureStorageService : ISecureStorageService
{
private readonly ProtectedLocalStorage _storage;
public WebSecureStorageService(ProtectedLocalStorage storage)
{
_storage = storage;
}
public async Task SetAsync(string key, string value) =>
await _storage.SetAsync(key, value);
public async Task<string?> GetAsync(string key)
{
var result = await _storage.GetAsync<string>(key);
return result.Success ? result.Value : null;
}
public async Task RemoveAsync(string key) =>
await _storage.DeleteAsync(key);
}
Những cải tiến trong .NET 10 cho Blazor Hybrid
.NET 10 mang đến khá nhiều cải tiến đáng chú ý. Mình sẽ chỉ tập trung vào những thứ thực sự hữu ích trong thực tế.
HybridWebView Initialization Events
Giờ đây bạn có thể can thiệp vào quá trình khởi tạo WebView — điều mà trước đây không dễ làm:
hybridWebView.WebViewInitializing += (sender, e) =>
{
// Cấu hình trước khi WebView được tạo
// Ví dụ: thiết lập custom user agent
};
hybridWebView.WebViewInitialized += (sender, e) =>
{
// Truy cập platform WebView sau khi sẵn sàng
#if IOS || MACCATALYST
// Tùy chỉnh WKWebView configuration
var wkWebView = e.WebView;
wkWebView.Configuration.Preferences.JavaScriptEnabled = true;
#elif ANDROID
// Tùy chỉnh Android WebView
var androidWebView = e.WebView;
androidWebView.Settings.DomStorageEnabled = true;
#endif
};
InvokeJavaScriptAsync không cần return type
.NET 10 thêm overload mới cho InvokeJavaScriptAsync dạng fire-and-forget. Đơn giản hơn, và exception từ JavaScript sẽ tự động được chuyển thành .NET exception:
// Fire-and-forget — không cần return type
await hybridWebView.InvokeJavaScriptAsync("updateDashboard", newData);
// JavaScript exception tự động trở thành .NET exception
try
{
await hybridWebView.InvokeJavaScriptAsync("riskyOperation");
}
catch (JSException ex)
{
_logger.LogError(ex, "JavaScript error occurred");
}
MediaPicker hỗ trợ chọn nhiều file
Tính năng nhỏ nhưng rất tiện: người dùng có thể chọn nhiều ảnh cùng lúc, kèm theo khả năng nén ảnh ngay trong API. Trước đây phải tự viết logic loop khá phiền — giờ thì không cần nữa.
Triển khai và Debug
Debug trên thiết bị thật
Tin vui là Blazor Hybrid app có thể debug bằng Visual Studio với đầy đủ breakpoint, watch, và hot reload. Vì code chạy native (không phải WebAssembly), trải nghiệm debug gần như ứng dụng .NET thông thường — khá thoải mái.
Để debug phần HTML/CSS/JS trong WebView:
- Windows: Sử dụng Edge DevTools (nhấn F12 khi WebView đang focus)
- Android: Bật
SetWebContentsDebuggingEnabled(true)và dùng Chrome DevTools quachrome://inspect - iOS: Bật Web Inspector trong Safari > Develop menu
Cấu hình cho Production
<!-- .csproj — Cấu hình build cho production -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<!-- Bật trimming để giảm kích thước app -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<!-- NativeAOT cho iOS (tùy chọn) -->
<PublishAot Condition="$([MSBuild]::GetTargetPlatformIdentifier(
'$(TargetFramework)')) == 'ios'">true</PublishAot>
<!-- Đảm bảo compiled bindings -->
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
Những lỗi thường gặp và cách khắc phục
1. "RenderMode is not supported" trên MAUI
Nguyên nhân: Component chỉ định render mode cụ thể (InteractiveServer, InteractiveWebAssembly) trong MAUI app.
Giải pháp: Sử dụng InteractiveRenderSettings helper class như mình đã trình bày ở trên, đặt render mode về null trong MauiProgram.cs.
2. CSS/JS không load trên MAUI
Nguyên nhân: File không được đánh dấu là MauiAsset hoặc nằm sai thư mục.
Giải pháp: Đảm bảo static assets nằm trong wwwroot/ của project MAUI hoặc được export đúng cách từ RCL. Lỗi này tưởng nhỏ nhưng mất thời gian tìm kiếm lắm.
3. Navigation không hoạt động giữa Web và MAUI
Nguyên nhân: Web app sử dụng NavigationManager với absolute URL, MAUI lại dùng relative path.
Giải pháp: Luôn dùng relative URL trong NavigationManager.NavigateTo() bên trong RCL component. Đây là quy tắc vàng.
4. WebView khởi động chậm
Nguyên nhân: WebView phải khởi tạo Blazor runtime mỗi lần app start.
Giải pháp: Giữ splash screen hiển thị trong lúc BlazorWebView đang tải. Giảm lượng CSS/JS cần load ban đầu. Người dùng thường chấp nhận splash screen 1-2 giây, nhưng sẽ khó chịu nếu thấy màn hình trắng.
Câu hỏi thường gặp (FAQ)
Blazor Hybrid có cần kết nối internet không?
Không. Blazor Hybrid chạy hoàn toàn trên thiết bị — tất cả code .NET và HTML/CSS đều được đóng gói cùng ứng dụng. Khác với Blazor Server (cần kết nối SignalR liên tục), Blazor Hybrid hoạt động offline hoàn toàn. Bạn chỉ cần internet khi ứng dụng gọi API bên ngoài hoặc tải dữ liệu từ server.
Blazor Hybrid có chậm hơn native .NET MAUI không?
Về render UI thì có, vì Blazor Hybrid chạy qua WebView nên sẽ có overhead nhất định so với native XAML controls. Nhưng business logic thì chạy cùng tốc độ — đều là code .NET native cả. Theo kinh nghiệm của mình, với những ứng dụng business/enterprise thông thường, sự khác biệt hiệu suất UI gần như không đáng kể. Chỉ khi bạn cần animation 60fps phức tạp hoặc render đồ họa nặng thì mới nên chuyển sang native MAUI.
Có thể trộn lẫn Blazor Hybrid và native MAUI controls không?
Có, hoàn toàn được. BlazorWebView chỉ là một control trong .NET MAUI — bạn có thể đặt nó cạnh các control native khác như ToolBar, TabBar, hay bất kỳ XAML layout nào. Nhiều ứng dụng production mà mình biết sử dụng Shell navigation native kết hợp với các page Blazor bên trong. Kết hợp tốt nhất của cả hai thế giới.
Blazor Hybrid có được publish lên App Store và Google Play không?
Hoàn toàn được. Blazor Hybrid app được đóng gói như ứng dụng native thông thường — file .apk/.aab cho Android và .ipa cho iOS. Apple và Google không phân biệt giữa app thuần native và app sử dụng WebView (miễn là tuân thủ guidelines). Quy trình submit và review giống hệt bất kỳ ứng dụng .NET MAUI nào khác.
Nên chọn Blazor Hybrid hay PWA?
PWA chạy trong browser sandbox, bị giới hạn bởi các API mà browser cho phép. Blazor Hybrid là ứng dụng native thật sự — có toàn quyền truy cập camera, Bluetooth, file system, background processing, push notification, và mọi API native khác. Nếu ứng dụng cần những tính năng này hoặc cần xuất hiện trên App Store thì chọn Blazor Hybrid. Còn nếu chỉ cần ứng dụng web có khả năng offline cơ bản thì PWA là quá đủ rồi.