عنوان:

‫افزایش کارایی و پاسخ‌گویی برنامه‌های Blazor WebAssembly با استفاده از Blazor Web Worker در دات‌نت 11


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۳۱ ۰۹:۳۵
آدرس: www.dntips.ir
این یک مکمل فوق‌العاده و در واقع حل‌کننده‌ی دقیق همان محدودیتی است که در مقاله‌ی قبلی به آن اشاره کردیم! مایکروسافت با معرفی تمپلیت Blazor Web Worker (که ابتدا با نام webworker معرفی شد و سپس به این نام تغییر یافت) یکی از بزرگ‌ترین چالش‌های برنامه‌های تک‌نخی مرورگر را حل کرده است. در ادامه، ابتدا داستان و فلسفه وجودی این قابلیت را بررسی می‌کنیم، سپس تفاوت کلیدی آن با IHostedService را در قالب یک جدول ساختاریافته ارائه می‌دهیم.

داستان این قابلیت چیست؟
همان‌طور که می‌دانید، مرورگرها کارهای خود را روی یک نخ (Thread) اصلی به نام UI Thread انجام می‌دهند. در Blazor WebAssembly نیز تمام کدهای #C شما به صورت پیش‌فرض روی همین تک‌نخ جاوااسکریپت اجرا می‌شوند.
اگر شما یک محاسبات سنگین (مانند پردازش تصویر، فشرده‌سازی فایل، یا الگوریتم‌های پیچیده ریاضی) را در برنامه انجام می‌دادید، این تک‌نخ کاملاً درگیر می‌شد و در نتیجه مرورگر قفل می‌کرد (Freeze می‌شد) و کاربر نمی‌توانست روی هیچ دکمه‌ای کلیک کند.
راه‌حل دنیای وب چیست؟ جاوااسکریپت قابلیتی به نام Web Worker دارد که اجازه می‌دهد یک اسکریپت در یک نخ کاملاً مجزا و پس‌زمینه، بدون دسترسی و آسیب زدن به UI اجرا شود.
کاربرد تمپلیت جدید دات‌نت 11: دات‌نت 11 با معرفی تمپلیت blazorwebworker ساختار فوق‌العاده راحتی فراهم کرده تا شما بتوانید کدهای #C خود را مستقیماً درون یک Web Worker واقعی مرورگر اجرا کنید. دات‌نت خودش تمام زیرساخت‌ها، ارتباطات بین نخ اصلی و نخ Worker (مسائل مربوط به Serialization و Interop) را مدیریت می‌کند و به شما یک کلاینت تمیز به نام WebWorkerClient با پشتیبانی از خطایابی، لغو عملیات (CancellationToken) و Timeout می‌دهد.

مقایسه تفصیلی: IHostedService در مقابل Blazor Web Worker

تفاوت این دو قابلیت بسیار حیاتی است و نباید با هم اشتباه گرفته شوند:
وجه تمایزIHostedService در Blazor WASMBlazor Web Worker
محل اجرا (Thread)روی همان نخ اصلی UI اجرا می‌شود.روی یک نخ پس‌زمینه واقعی (Separate Thread) اجرا می‌شود.
هدف اصلیمدیریت چرخه‌ی حیات برنامه، کارهای دوره‌ای سبک (مانند زدن به API هر ۵ دقیقه).انجام محاسبات سنگین و طولانی‌مدت بدون بلاک کردن UI.
تأثیر بر پاسخ‌گویی UIاگر کار سنگینی انجام دهد، رابط کاربری قفل می‌شود.رابط کاربری کاملاً پاسخ‌گو (Responsive) باقی می‌ماند.
نحوه ارتباطدسترسی مستقیم به کانتینر DI و وضعیت برنامه دارد.از طریق پیام‌رسانی ساختاریافته (InvokeAsync) با نخ اصلی تبادل داده می‌کند.

معماری و قابلیت‌های تمپلیت Blazor Web Worker
این تمپلیت جدید، زیرساخت کامل ارتباطی (Communication Infrastructure) میان برنامه اصلی Blazor و محیط مجزای Web Worker را به همراه الگوهای طراحی استاندارد ایجاد می‌کند. دات‌نت 11 با معرفی کلاس WebWorkerClient مبتنی بر الگوی کارخانه (Factory Pattern)، مدیریت طول عمر و فراخوانی متدهای Worker را به شدت ساده کرده است.

تغییرات و بهینه‌سازی‌ها در دات‌نت 11
در آخرین بروزرسانی‌های دات‌نت 11، این تمپلیت تغییرات مهمی را برای افزایش پایداری و کاربردپذیری دریافت کرده است:
  • تغییر نام به Blazor Web Worker: جهت تفکیک واضح و مشخص شدن وابستگی این تمپلیت به اکوسیستم Blazor.
  • پشتیبانی از InvokeVoidAsync: اضافه شدن قابلیت Fire-and-forget برای زمان‌هایی که نیاز به فراخوانی یک متد بدون بازگشت هیچ‌گونه مقداری از سمت Worker است.
  • پشتیبانی از لغو و زمان استراحت (Cancellation & Timeout): امکان پاس دادن CancellationToken به فرآیند ساخت یا اجرای متدها در Worker، جهت از بین بردن تمیزِ (Tear down) سرویس‌های پس‌زمینه‌ای که دچار بن‌بست (Stuck) شده‌اند.

نمونه پیاده‌سازی عملی

۱. ایجاد پروژه Worker
توسعه‌دهندگان می‌توانند با دستور زیر ساختار پروژه را ایجاد کنند:
dotnet new blazorwebworker -o MyApp.Worker

۲. نمونه کد فراخوانی در کامپوننت Blazor
در سمت کلاینت (نخ اصلی)، از WebWorkerClient برای ساخت ورکر و فراخوانی متدهای سی‌شارپی که با اتریبیوت [JSExport] در پروژه ورکر نشانه‌گذاری شده‌اند، استفاده می‌شود:
@page "/heavy-computation"
@inject IJSRuntime JSRuntime

<h3>پردازش سنگین در نخ پس‌زمینه</h3>

<button @onclick="RunHeavyTask">شروع محاسبات سنگین</button>
<p>نتیجه: @_result</p>

@code {
    private string _result = "آماده برای پردازش...";

    private async Task RunHeavyTask()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // تنظیم تایم‌اوت ۳۰ ثانیه‌ای
        
        // ایجاد یک نمونه از ورکر به صورت آسنکرون
        await using var worker = await WebWorkerClient.CreateAsync(JSRuntime, cts.Token);
        
        _result = "در حال محاسبه...";
        
        // فراخوانی متد سی‌شارپ مستقر در Web Worker بدون مسدود شدن UI
        _result = await worker.InvokeAsync<string>(
            "MyApp.MyWorker.Greet", 
            ["World"], 
            cts.Token
        );
    }
}

نکات کلیدی
برای استفاده اصولی از Blazor Web Worker، درک جزییات زیر اهمیت فنی بالایی دارد:
  • جداسازی کامل حافظه (Isolated Memory): یک Web Worker در فضای حافظه اختصاصی خود اجرا می‌شود. این بدان معناست که شما نمی‌توانید نمونه (Instance) اشیاء پیچیده فعال در برنامه اصلی را مستقیماً به ورکر پاس بدهید. داده‌ها در زمان ارسال و دریافت مابین نخ اصلی و ورکر، فرآیند سریال‌سازی (Serialization) را طی می‌کنند. بنابراین، ورودی‌ها و خروجی‌های متدهای ورکر باید سبک و قابلیت سریالایز شدن بالایی داشته باشند.
  • عدم دسترسی به DOM: کدهایی که درون Web Worker اجرا می‌شوند، دسترسی مستقیم به DOM مرورگر یا کامپوننت‌های Blazor ندارند. وظیفه آن‌ها صرفاً دریافت داده خام، انجام پردازش و بازگرداندن داده خام به نخ اصلی است.
  • مدیریت بهینه منابع: ایجاد یک Web Worker جدید در مرورگر هزینه‌بر (Heavyweight) است. بهتر است برای کارهای مکرر، نمونه‌ی ورکر را به صورت طولانی‌مدت (Long-lived) نگه‌داری کنید و پس از اتمام کامل کارها، حتماً با فراخوانی متد DisposeAsync آن را متوقف سازید تا از نشت حافظه (Memory Leak) جلوگیری شود.

نتیجه‌گیری
تمپلیت Blazor Web Worker در دات‌نت 11، مرزهای کارایی برنامه‌های تحت وب مبتنی بر WebAssembly را جابجا کرده است. این قابلیت با فراهم کردن امکان انتقال پردازش‌های محاسباتی سنگین به نخ‌های مجزا، تضمین می‌کند که رابط کاربری برنامه‌های Blazor همواره نرم و پاسخ‌گو باقی بماند. ترکیب این ابزار (برای کارهای سنگین محاسباتی) در کنار IHostedService (برای مدیریت چرخه‌ی حیات فرآیندهای سبک دوره‌ای)، جعبه ابزار توسعه‌دهندگان دات‌نت را برای خلق برنامه‌های وب در سطح Enterprise کامل‌تر از همیشه کرده است.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۳/۳۱ ۱۰:۰۶
    پیاده‌سازی گام‌به‌گام پردازش‌های ناهمگام در Blazor WebWorker: بررسی یک نمونه کاربردی در دات‌نت 11

    در معماری برنامه‌های سنتی Blazor WebAssembly، فراخوانی متدهای سنگین به معنای درگیر شدن مستقیم تنها نخ اجرای جاوااسکریپت (UI Thread) است. برای ملموس‌تر شدن چگونگی حل این مشکل توسط تمپلیت جدید Blazor Web Worker در دات‌نت 11، این مقاله به بررسی یک سناریوی واقعی می‌پردازد: جایگزینی فرآیند دریافت داده‌های هواشناسی با یک الگوریتم محاسباتی سنگین محاسباتی در پس‌زمینه. در ادامه، نحوه پیکربندی، ساختار متدهای صادراتی ([JSExport]) و پیاده‌سازی الگوی بهینه کشینگ (Caching) برای جلوگیری از بارگذاری مکرر رانتایم دات‌نت را بررسی می‌کنیم.

    ۱. تنظیمات ساختاری پروژه (Project Configuration)
    پس از ایجاد پروژه با دستور dotnet new blazorwebworker، برای استفاده از قابلیت‌های پیشرفته پل ارتباطی بین دات‌نت و جاوااسکریپت، باید اجازه استفاده از کدهای ناامن (Unsafe Code) را در فایل پروژه .csproj برنامه اصلی Blazor فعال کنیم:
    <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
      <PropertyGroup>
        <TargetFramework>net11.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\BlazorWebWorker\BlazorWebWorker.csproj" />
      </ItemGroup>
    </Project>

    ۲. تعریف متد محاسباتی درون Web Worker
    کدهایی که قرار است درون نخ پس‌زمینه ورکر اجرا شوند، دارای محدودیت‌های خاصی در تبادل داده هستند. این متدها باید به صورت static partial تعریف شده، با اتریبیوت [JSExport] نشانه‌گذاری شوند و خروجی آن‌ها ترجیحاً از نوع انواع داده اولیه (Primitive) یا string (به صورت متون سریالایز شده JSON) باشد.
    using System.Text.Json;
    using System.Runtime.Versioning;
    using System.Runtime.InteropServices.JavaScript;
    
    namespace BlazorWebApp;
    
    [SupportedOSPlatform("browser")]
    public static partial class WorkerMethods
    {
        [JSExport]
        public static string GetForecasts(int count)
        {
            // شبیه‌سازی یک پردازش سنگین محاسباتی که اجرای آن ۵ ثانیه زمان می‌برد.
            // اگر این کد روی نخ اصلی اجرا می‌شد، مرورگر کاملاً قفل (Freeze) می‌شد.
            Thread.Sleep(5_000); 
    
            // تولید داده‌های خام هواشناسی
            var forecasts = Enumerable.Range(1, count).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = "تولید شده در وب‌ورکر"
            }).ToArray();
    
            // تبدیل به رشته متنی جهت انتقال امن از فنس حافظه ورکر به مروگر
            return JsonSerializer.Serialize(forecasts); 
        }
    }

    ۳. مدیریت بهینه طول عمر ورکر با الگوی Caching
    از آنجا که راه‌اندازی هر نمونه از Web Worker نیازمند بارگذاری مجدد فایل‌های پایه‌ای رانتایم دات‌نت (_framework/dotnet.js و اسمبلی‌ها) در یک امپدانس حافظه مستقل است، متد CreateAsync عملیاتی زمان‌بر به شمار می‌رود. برای غلبه بر این اورهد (Overhead)، استفاده از یک الگوی کشینگ استاتیک بسیار حیاتی است:
    public static class WorkerClientContainer
    {
        private static WebWorkerClient? _cachedClient;
    
        public static async Task<WebWorkerClient> GetOrCreateClientAsync(IJSRuntime jsRuntime)
        {
            // بازگرداندن کلاینت موجود در صورت مقداردهی قبلی
            if (_cachedClient is not null)
            {
                return _cachedClient;
            }
    
            // ایجاد نمونه برای اولین بار (هزینه‌بر اما فقط یک‌بار)
            _cachedClient = await WebWorkerClient.CreateAsync(jsRuntime);
            return _cachedClient;
        }
    }

    ۴. فراخوانی ورکر در کامپوننت Razor
    در نهایت، در کامپوننت فرانت‌اند بدون نگرانی از مسدود شدن رابط کاربری، متد را فراخوانی کرده و ساختار ناهمگام را پیاده می‌کنیم:
    @page "/weather"
    @using BlazorWebWorker
    @inject IJSRuntime JsRuntime
    
    <h3>وضعیت آب و هوا (پردازش در ورکر)</h3>
    
    @if (forecasts == null)
    {
        <p><em>در حال محاسبات سنگین در نخ پس‌زمینه (UI قفل نشده است)...</em></p>
    }
    else
    {
        }
    
    @code {
        private WeatherForecast[]? forecasts;
    
        protected override async Task OnInitializedAsync()
        {
            // دریافت کلاینت ورکر از کش
            var worker = await WorkerClientContainer.GetOrCreateClientAsync(JsRuntime);
    
            // مشخص کردن آدرس دقیق متد صادراتی دات‌نت
            const string methodSignature = "BlazorWebApp.WorkerMethods.GetForecasts";
            const int requestedRows = 5;
            
            // فرستادن درخواست به ورکر. متد InvokeAsync به صورت خودکار 
            // خروجی رشته را به آبجکت سی‌شارپ دیسریالایز می‌کند.
            forecasts = await worker.InvokeAsync<WeatherForecast[]>(methodSignature, args: [requestedRows]);
        }
    }

    تحلیل فنی زیرساخت (Behind the Scenes)
    مایکروسافت این فرآیند پیچیده را به واسطه دو فایل جاوااسکریپت درونی مدیریت می‌کند:
    • dotnet-web-worker-client.js: در نقش کلاینتِ نخ اصلی عمل کرده، سیستم شناسه‌گذاری درخواست‌ها (requestId) را هندل می‌کند و مدیریت Promiseها را به عهده دارد.
    • dotnet-web-worker.js: به عنوان هسته اصلی ورکر مستقر در پس‌زمینه عمل می‌کند. این اسکریپت رانتایم سبک دات‌نت را لود کرده، با متد getAssemblyExports متدهای مجاز سی‌شارپ را استخراج می‌کند و از طریق self.postMessage پاسخ‌ها را در قالب Transferable Objects (بدون کپی شدن‌های سنگین در حافظه) به نخ اصلی پس می‌فرستد.

    نتیجه‌گیری
    این مدل معماری ایده آل‌ترین حالت برای تفکیک وظایف (Separation of Concerns) در بستر مرورگر است. توسعه‌دهنده با این متدولوژی می‌تواند اپلیکیشن‌های تحت وب با توان پردازشی بالا نظیر ابزارهای مهندسی، پردازشگرهای فایلی و نمودارهای بلادرنگ (Real-time) را با همان زبان #C و بدون فدا کردن نرخ فریم رابط کاربری (UI Frame Rate) توسعه دهد.