.NET trên Web Workers
Nguồn: .NET on Web Workers
Các ứng dụng web hiện đại thường yêu cầu các tác vụ tính toán nặng có thể chặn luồng UI chính, dẫn đến trải nghiệm người dùng kém. Web Workers cung cấp giải pháp cho vấn đề này bằng cách cho phép JavaScript (JS) chạy trên các luồng riêng biệt. Với .NET WebAssembly (Wasm), bạn có thể chạy code C# trong Web Workers, kết hợp lợi ích hiệu năng của code đã biên dịch với mô hình thực thi không chặn của các luồng nền.
Phương pháp này đặc biệt có giá trị khi bạn cần thực hiện các phép tính phức tạp, xử lý dữ liệu, hoặc logic nghiệp vụ mà không yêu cầu thao tác DOM trực tiếp. Thay vì viết lại các thuật toán trong JS, bạn có thể duy trì codebase .NET hiện có và thực thi nó hiệu quả trong nền trong khi frontend React.js vẫn phản hồi tốt.
Lưu ý: Với .NET 8/9/10, hãy làm theo các bước wasmbrowser thủ công trong bài viết này. Để xem hướng dẫn dành riêng cho Blazor, xem ASP.NET Core Blazor with .NET on Web Workers.
Bài viết này trình bày phương pháp React sử dụng dự án .NET WebAssembly độc lập.
Ứng dụng mẫu
Khám phá triển khai hoàn chỉnh trong kho mẫu Blazor trên GitHub. Mẫu dành cho .NET 10 trở lên và có tên DotNetOnWebWorkersReact.
Điều kiện tiên quyết và cài đặt
Trước khi bắt đầu triển khai, hãy đảm bảo các công cụ cần thiết đã được cài đặt.
.NET SDK 8.0 trở lên là bắt buộc. Nếu WebAssembly build tools chưa được cài đặt, hãy chạy:
dotnet workload install wasm-tools dotnet workload install wasm-experimental
Đối với frontend React.js, Node.js và npm phải được cài đặt.
Tạo một ứng dụng React mới:
npx create-react-app react-app cd react-app
Tạo dự án .NET WebAssembly
Tạo một dự án WebAssembly browser mới để đóng vai trò là Web Worker:
dotnet new wasmbrowser -o WebWorkersOnReact cd WebWorkersOnReact dotnet add package QRCoder
Chỉnh sửa file Program.cs để thiết lập điểm vào (entry point) Web Worker và xử lý tin nhắn:
using System;
using System.Runtime.InteropServices.JavaScript;
using QRCoder;
using System.Linq;
public partial class QRGenerator
{
private static readonly int MAX_QR_SIZE = 20;
[JSExport]
internal static byte[] Generate(string text, int qrSize)
{
if (qrSize >= MAX_QR_SIZE)
{
throw new Exception(
$"QR code size must be less than {MAX_QR_SIZE}. Try again.");
}
QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(
text, QRCodeGenerator.ECCLevel.Q);
BitmapByteQRCode qrCode = new BitmapByteQRCode(qrCodeData);
return qrCode.GetGraphic(qrSize);
}
}Thêm file wwwroot/worker.js với code interop (tương tác) giữa C# và JS:
import { dotnet } from './_framework/dotnet.js'
let assemblyExports = null;
let startupError = undefined;
try {
const { getAssemblyExports, getConfig } = await dotnet.create();
const config = getConfig();
assemblyExports = await getAssemblyExports(config.mainAssemblyName);
}
catch (err) {
startupError = err.message;
}
self.addEventListener('message', async function(e) {
try {
if (!assemblyExports) {
throw new Error(startupError || "worker exports not loaded");
}
let result = null;
switch (e.data.command) {
case "generateQR":
const size = Number(e.data.size);
const text = e.data.text;
if (size === undefined || text === undefined)
new Error("Inner error, got empty QR generation data from React");
result = assemblyExports.QRGenerator.Generate(text, size);
break;
default:
throw new Error("Unknown command: " + e.data.command);
}
self.postMessage({
command: "response",
requestId: e.data.requestId,
result,
});
}
catch (err) {
self.postMessage({
command: "response",
requestId: e.data.requestId,
error: err.message,
});
}
}, false);Build dự án worker:
dotnet build
Thiết lập ứng dụng React
Để kiểm tra nhanh, hãy sao chép output của worker vào các file tĩnh của ứng dụng React thủ công. Với ứng dụng thực tế, hãy tự động hóa các bước sao chép này bằng npm script hoặc bước build khác.
Tạo một Web Worker file client.js để nhận tin nhắn từ dotnet:
const dotnetWorker = new Worker('../../qr/wwwroot/worker.js', { type: "module" } );
dotnetWorker.addEventListener('message', async function (e) {
switch (e.data.command) {
case "response":
if (!e.data.requestId) {
console.error("No requestId in response from worker");
}
const request = pendingRequests[e.data.requestId];
delete pendingRequests[e.data.requestId];
if (e.data.error) {
request.reject(new Error(e.data.error));
}
request.resolve(e.data.result);
break;
default:
console.log('Worker said: ', e.data);
break;
}
}, false);Kết nối chức năng này với UI và thêm nút kích hoạt generateQR:
export async function generateQR(text, size) {
const response = await sendRequestToWorker({
command: "generateQR",
text: text,
size: size
});
const blob = new Blob([response], { type: 'image/png' });
return URL.createObjectURL(blob);
}
function sendRequestToWorker(request) {
pendingRequestId++;
const promise = new Promise((resolve, reject) => {
pendingRequests[pendingRequestId] = { resolve, reject };
});
dotnetWorker.postMessage({
...request,
requestId: pendingRequestId
});
return promise;
}Cân nhắc về hiệu năng và tối ưu hóa
Khi làm việc với .NET trên Web Workers, hãy cân nhắc các chiến lược tối ưu hóa chính sau:
- Giảm thiểu truyền dữ liệu: Chỉ tuần tự hóa dữ liệu cần thiết giữa luồng chính và worker để giảm chi phí giao tiếp.
- Gom nhóm thao tác (Batch operations): Nhóm nhiều phép tính lại với nhau thay vì gửi từng yêu cầu riêng lẻ.
- Quản lý bộ nhớ (Memory management): Chú ý đến việc sử dụng bộ nhớ trong môi trường WebAssembly, đặc biệt với các worker chạy lâu dài.
- Chi phí khởi động (Startup cost): Việc khởi tạo WebAssembly có chi phí overhead, vì vậy nên ưu tiên các worker duy trì liên tục thay vì tạo/hủy thường xuyên.
Xem ứng dụng mẫu để xem minh họa các khái niệm trên.