عنوان:

‫بهینه‌سازی رندر در Blazor


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۱/۲۰ ۰۸:۳۱
آدرس: www.dntips.ir
مقدمه
یکی از چالش‌های رایج در مسیر توسعه‌ی برنامه‌های Blazor، مدیریت رندر (Rendering) کامپوننت‌ها است. گاهی اوقات، یک کامپوننت بیش از حد انتظار رندر می‌شود که می‌تواند عملکرد برنامه را تحت تأثیر قرار دهد و تجربه کاربری را تضعیف کند. این مقاله با هدف آموزش بهینه‌سازی رندر در Blazor نوشته شده است و بر اساس تجربه‌ای واقعی از تحلیل و رفع مشکل رندر غیرضروری یک کامپوننت، به شما کمک می‌کند تا درک بهتری از رفتار Blazor داشته باشید و راهکارهایی عملی برای بهبود آن پیاده‌سازی کنید. در این مسیر، مفاهیم کلیدی مانند چرخه حیات کامپوننت (Component Lifecycle)، متد ShouldRender و استفاده از ابزارهای دیباگ (Debugging Tools) بررسی می‌شوند.

درک مشکل رندر بیش‌ازحد
در Blazor، رندر یک کامپوننت زمانی رخ می‌دهد که فریم‌ورک تشخیص دهد وضعیت (State) آن تغییر کرده است. این تغییر می‌تواند ناشی از به‌روزرسانی پارامترها، اجرای رویدادها یا فراخوانی دستی متد StateHasChanged باشد. اما گاهی اوقات، رندر بیش از حد اتفاق می‌افتد، حتی وقتی تغییر واقعی در وضعیت وجود ندارد. به عنوان مثال، فرض کنید یک کامپوننت ساده دارید که یک لیست کشویی (Dropdown) را نمایش می‌دهد:
@inject NavigationManager NavigationManager

<select @onchange="OnValueChanged">
    <option value="">انتخاب کنید</option>
    @foreach (var item in Items)
    {
        <option value="@item">@item</option>
    }
</select>

@code {
    [Parameter] public List<string> Items { get; set; } = new();
    [Parameter] public EventCallback<string> ValueChanged { get; set; }

    private async Task OnValueChanged(ChangeEventArgs e)
    {
        string value = e.Value?.ToString() ?? "";
        await ValueChanged.InvokeAsync(value);
        NavigationManager.NavigateTo("/new-page");
    }
}
در این کد، وقتی کاربر گزینه‌ای را انتخاب می‌کند، متد OnValueChanged اجرا می‌شود و پس از آن، به صفحه جدیدی هدایت می‌شود. اما اگر از ابزاری مانند Console.WriteLine در متدهای چرخه حیات استفاده کنید، ممکن است متوجه شوید که این کامپوننت چندین بار رندر می‌شود—گاهی حتی تا 20 بار! این رفتار غیرمنتظره می‌تواند دلایل مختلفی داشته باشد که در ادامه بررسی می‌کنیم.

دلایل رندر غیرضروری
چرخه حیات Blazor و رویدادها:
Blazor به‌طور پیش‌فرض پس از اجرای هر رویداد (Event) مانند @onchange ، متد StateHasChanged را فراخوانی می‌کند تا مطمئن شود رابط کاربری به‌روز است. این رفتار در کلاس پایه ComponentBase تعریف شده است. در مثال بالا، پس از اجرای OnValueChanged، Blazor یک رندر اضافی را آغاز می‌کند، حتی اگر هدایت (Navigation) به صفحه جدید بلافاصله انجام شود.

پررندر (Prerendering) در Blazor Server:
اگر از Blazor Server با حالت InteractiveServer یا ServerPrerendered استفاده می‌کنید، کامپوننت‌ها ابتدا روی سرور به‌صورت استاتیک رندر می‌شوند و سپس با برقراری اتصال SignalR، دوباره رندر می‌شوند. این دوبار رندر شدن گاهی اوقات با تغییرات وضعیت ترکیب شده و تعداد رندرها را افزایش می‌دهد.

به‌روزرسانی پارامترها:
وقتی پارامترهای یک کامپوننت (مانند Items در مثال) تغییر می‌کنند، متد OnParametersSet اجرا می‌شود و رندر جدیدی آغاز می‌شود. اگر این پارامترها به‌صورت مرجع (Reference Type) باشند و Blazor نتواند تغییرات واقعی را تشخیص دهد، رندر غیرضروری رخ می‌دهد.

رفتار نادرست در متدهای async:
در متدهای ناهمگام (Asynchronous) مانند OnValueChanged، اگر await به‌درستی مدیریت نشود، Blazor ممکن است در نقاط مختلف اجرای Task رندر را آغاز کند. این رفتار به دلیل تغییر وضعیت TaskStatus در چرخه اجرای تسک است.

ابزارهای تشخیص مشکل
برای شناسایی دلایل رندر بیش‌ازحد، می‌توانید از تکنیک‌های ساده‌ای استفاده کنید:
  • لاگ‌گذاری در چرخه حیات: با افزودن Console.WriteLine به متدهای OnInitialized، OnParametersSet و ShouldRender، می‌توانید تعداد و زمان رندرها را ردیابی کنید. مثلاً:
protected override void OnInitialized()
{
    Console.WriteLine("OnInitialized called");
}

protected override void OnParametersSet()
{
    Console.WriteLine("OnParametersSet called");
}

protected override bool ShouldRender()
{
    Console.WriteLine("ShouldRender called");
    return true;
}
  • ابزارهای توسعه‌دهنده مرورگر: در Blazor WebAssembly، از تب Network یا Performance در مرورگر استفاده کنید تا درخواست‌ها و زمان‌بندی رندرها را بررسی کنید.
  • دیباگر Visual Studio: نقاط شکست (Breakpoints) را در متدهای کلیدی قرار دهید تا جریان اجرا را دقیق‌تر تحلیل کنید.

راهکارهای بهینه‌سازی رندر

استفاده از ShouldRender:
متد ShouldRender به شما اجازه می‌دهد کنترل کنید که آیا کامپوننت باید رندر شود یا خیر. با بازگرداندن false در شرایط خاص، می‌توانید رندر غیرضروری را متوقف کنید. برای مثال:
private bool shouldRender = true;

protected override void OnParametersSet()
{
    Console.WriteLine("OnParametersSet called");
    shouldRender = true; // فقط وقتی پارامترها تغییر واقعی دارند
}

protected override bool ShouldRender()
{
    Console.WriteLine("ShouldRender called");
    if (!shouldRender)
    {
        return false;
    }
    shouldRender = false; // پس از اولین رندر، تا تغییر بعدی رندر نشود
    return true;
}
در اینجا، پس از اولین رندر، تا زمانی که تغییر جدیدی رخ ندهد، رندر متوقف می‌شود.

مدیریت رویدادها با دقت:
اگر رویدادی مانند هدایت (Navigation) باعث خروج از کامپوننت می‌شود، نیازی به رندر اضافی نیست. می‌توانید از یک پرچم (Flag) استفاده کنید:
private bool isNavigating = false;

private async Task OnValueChanged(ChangeEventArgs e)
{
    string value = e.Value?.ToString() ?? "";
    await ValueChanged.InvokeAsync(value);
    isNavigating = true;
    NavigationManager.NavigateTo("/new-page");
}

protected override bool ShouldRender()
{
    return !isNavigating;
}

بهینه‌سازی متدهای async:
در متدهای ناهمگام، از StateHasChanged به‌صورت دستی استفاده کنید تا رندر فقط در نقاط لازم انجام شود:
private async Task LoadDataAsync()
{
    await Task.Delay(1000); // شبیه‌سازی دریافت داده
    progress = 50;
    StateHasChanged(); // رندر دستی در میانه کار
    await Task.Delay(1000);
    progress = 100;
}

کنترل پررندر:
در Blazor Server، اگر پررندر باعث رندر اضافی می‌شود، می‌توانید آن را غیرفعال کنید. در فایل Program.cs یا در سطح کامپوننت:
@rendermode InteractiveServer(false) // غیرفعال کردن پررندر

استفاده از نوع‌های شناخته‌شده (Known Types):
Blazor برای نوع‌های ساده (Primitive Types) مانند int یا string، تغییرات را به‌خوبی تشخیص می‌دهد و رندر غیرضروری را کاهش می‌دهد. در مقابل، برای نوع‌های مرجع (Reference Types)، بهتر است تغییرات را صراحتاً بررسی کنید.

نتیجه‌گیری
مدیریت رندر در Blazor مهارتی است که با درک عمیق‌تر چرخه حیات و رفتار فریم‌ورک به دست می‌آید. رندر بیش‌ازحد نه‌تنها منابع را هدر می‌دهد، بلکه می‌تواند عملکرد برنامه را تحت تأثیر قرار دهد. با استفاده از ابزارهای تشخیص مانند لاگ‌گذاری و دیباگر، و پیاده‌سازی راهکارهایی مانند ShouldRender، کنترل رویدادها و بهینه‌سازی متدهای async، می‌توانید این مشکل را به‌طور مؤثری حل کنید.


مشاهده مطلب اصلی

نظرات

  • جاهد در ۱۴۰۴/۰۱/۲۲ ۲۱:۵۳
    در یک api تستی توسط سیگنال آر تعداد زیادی ایونت به دوبرنامه blazor و vuejs ارسال شد.تنها کاری هم که Ui انجام میده نمایش آخرین ایونت ارسال شده هست.نتیجه این شد که اپدیت صفحه در vue بسیار سریعتر و روانتر از blazor هست.احساس بنده به بلیزور در صفحات وب به این شکل هست که نرم وروان اجرا نمیشه به خصوص در مرورگرهای موبایل.(البته در react و valinajs هم مشابه vue سرعت بسیار سریعتر از blazor بود)نمونه کدها هم در ذیل قرار داده میشه
    بک اند:
    [HttpPost("send-message")]
    public async Task<IActionResult> SendMessage()
    {
       for (int i = 0; i <= 100000; i++)
       {
          await _hubContext.Clients.All.SendAsync("ReceiveMessage",i );
       }
       return Ok();
    }

    blazor:
    @using Microsoft.AspNetCore.SignalR.Client
    @inject HubConnection HubConnection
    <p @key="1"> <span>Message from server:</span> @model.LastOrDefault()</p>
    
    @code {
       private string message = "Waiting for messages...";
       List<string> model = new List<string>();
       protected override async Task OnInitializedAsync()
       {
          // متصل شدن به SignalR Hub
          HubConnection.On<string>("ReceiveMessage", async (receivedMessage) =>
          {
             model.Add(receivedMessage);
             await InvokeAsync(() =>
             {
               StateHasChanged();
             });
           });
          await HubConnection.StartAsync();
       }
    }
    vuejs:
    <script setup>
    import WelcomeItem from './WelcomeItem.vue'
    import DocumentationIcon from './icons/IconDocumentation.vue'
    import ToolingIcon from './icons/IconTooling.vue'
    import EcosystemIcon from './icons/IconEcosystem.vue'
    import CommunityIcon from './icons/IconCommunity.vue'
    import SupportIcon from './icons/IconSupport.vue'
    </script>
    
    <template>
    <div>
    <p>{{ lastItem }}</p>
    </div>
    </template>
    
    <script>
    // Import SignalR library
    import * as signalR from '@microsoft/signalr';
    
    export default {
        data() {
            return {
                items: [],
                message: 'Waiting for messages...',
                connection: null, // The SignalR connection object
            };
        },
        computed: {
            lastItem() {
                return this.items.length - 1;
            }
        },
        mounted() {
            // Create a new connection
            this.connection = new signalR.HubConnectionBuilder()
                .withUrl('http://localhost:5154/signalrhub') // URL of the SignalR hub (server-side)
                .build();
    
            // Start the connection
            this.connection.start()
                .then(() => {
                    console.log('Connected to SignalR Hub!');
                })
                .catch(err => {
                    console.error('Error while starting connection: ' + err);
                });
    
            // Listen for messages from the server
            this.connection.on('ReceiveMessage', (message) => {
                this.items.push(message);
            });
        },
        beforeDestroy() {
            // Stop the connection when the component is destroyed
            if (this.connection) {
                this.connection.stop();
            }
        }
    };
    </script>

    • وحید نصیری در ۱۴۰۴/۰۱/۲۲ ۲۲:۴۰
      برای بهینه‌سازی این برنامه Blazor که از SignalR برای دریافت و نمایش پیام‌ها استفاده می‌کند، باید به عملکرد سمت کلاینت، مدیریت منابع، و تجربه کاربری توجه ویژه‌ای داشت:

      1. بهینه‌سازی ارسال پیام‌ها در سمت سرور
      مشکل: ارسال 100,000 پیام به‌صورت متوالی در یک حلقه، فشار زیادی به شبکه و کلاینت وارد می‌کند و ممکن است باعث تأخیر یا قطعی شود.
      راه‌حل‌ها:
      ارسال دسته‌ای (Batching): پیام‌ها را به‌صورت دسته‌های کوچک‌تر (مثلاً 100 پیام) ارسال کنید و بین دسته‌ها تأخیر اضافه کنید.
      public async Task<IActionResult> SendMessage()
      {
          int batchSize = 100;
          for (int i = 0; i <= 100000; i += batchSize)
          {
              var batch = Enumerable.Range(i, Math.Min(batchSize, 100001 - i)).ToList();
              await _hubContext.Clients.All.SendAsync("ReceiveMessage", batch);
              await Task.Delay(100); // تأخیر برای کاهش فشار
          }
          return Ok();
      }

      2. کاهش بار رندر در کلاینت
      مشکل: فراخوانی StateHasChanged برای هر پیام، باعث رندر مکرر و کندی رابط کاربری می‌شود، به‌ویژه با حجم بالای پیام‌ها (مهم!).
      راه‌حل‌ها:
      • بافر کردن پیام‌ها: پیام‌ها را در یک بافر موقت ذخیره کنید و به‌صورت دوره‌ای (مثلاً هر 500 میلی‌ثانیه) رندر کنید.
      private List<string> buffer = new List<string>();
      private bool isRendering = false;
      
      protected override async Task OnInitializedAsync()
      {
          HubConnection.On<string>("ReceiveMessage", async (receivedMessage) =>
          {
              buffer.Add(receivedMessage);
              if (!isRendering)
              {
                  isRendering = true;
                  await RenderBufferedMessages();
              }
          });
          await HubConnection.StartAsync();
      }
      
      private async Task RenderBufferedMessages()
      {
          while (buffer.Any())
          {
              model.AddRange(buffer);
              buffer.Clear();
              await InvokeAsync(StateHasChanged);
              await Task.Delay(500); // فرکانس رندر
          }
          isRendering = false;
      }
      • استفاده از Virtualization: برای نمایش لیست پیام‌ها، از کامپوننت Virtualize استفاده کنید تا فقط پیام‌های قابل‌مشاهده رندر شوند:
      <div style="height: 400px; overflow-y: auto;">
          <Virtualize Items="model" Context="message">
              <p>@message</p>
          </Virtualize>
      </div>

      3. مدیریت حافظه
      مشکل: ذخیره 100,000 پیام در لیست model می‌تواند حافظه کلاینت را پر کند.
      راه‌حل‌ها:
      • محدود کردن تعداد پیام‌ها: حداکثر تعداد پیام‌ها را محدود کنید (مثلاً 1000 پیام آخر).
      private const int MaxMessages = 1000;
      
      HubConnection.On<string>("ReceiveMessage", async (receivedMessage) =>
      {
          buffer.Add(receivedMessage);
          if (model.Count >= MaxMessages)
          {
              model.RemoveRange(0, model.Count - MaxMessages + buffer.Count);
          }
          if (!isRendering)
          {
              isRendering = true;
              await RenderBufferedMessages();
          }
      });
      • استفاده از Queue: به‌جای List<string>، از Queue<string> برای مدیریت FIFO پیام‌ها استفاده کنید.

      4. بهبود تجربه کاربری
      • نمایش لیست پیام‌ها: به‌جای نمایش فقط آخرین پیام (LastOrDefault)، یک لیست اسکرول‌شونده نمایش دهید.
      <div style="height: 400px; overflow-y: auto;">
          <Virtualize Items="model" Context="message">
              <p>@message</p>
          </Virtualize>
      </div>
      • نمایش وضعیت اتصال: وضعیت اتصال SignalR را به کاربر نشان دهید.
      private string connectionStatus = "Connecting...";
      
      protected override async Task OnInitializedAsync()
      {
          HubConnection.StateChanged += async (state) =>
          {
              connectionStatus = state.ToString();
              await InvokeAsync(StateHasChanged);
          };
          // ... بقیه کدها
      }

      5. مدیریت پایداری اتصال
      • اتصال مجدد خودکار: در صورت قطع شدن اتصال، مکانیزم اتصال مجدد پیاده‌سازی کنید.
      HubConnection.Closed += async (error) =>
      {
          connectionStatus = "Disconnected. Reconnecting...";
          await InvokeAsync(StateHasChanged);
          await Task.Delay(5000);
          await HubConnection.StartAsync();
      };
      • مدیریت خطاها: پیام‌های نامعتبر را بررسی کنید تا برنامه کرش نکند.

      6. بهینه‌سازی SignalR
      • ارسال هدفمند: اگر پیام‌ها برای کلاینت‌های خاصی هستند، از Clients.Group یا Clients.User استفاده کنید (مهم!).
      • پیکربندی هاب: حداکثر اندازه پیام و تعداد اتصال‌ها را در SignalR تنظیم کنید.