Blazor Load Testing
Blazor Server ใช้ SignalR WebSocket สำหรับ Real-time UI Updates ต้องทดสอบทั้ง WebSocket Connections และ HTTP API Blazor WASM ทดสอบ Backend API และ Initial Load Time
Load Testing Strategy สำหรับ Blazor ครอบคลุม SignalR Connections, API Endpoints, Browser Rendering และ Resource Usage วัดผลด้วย Metrics ที่เหมาะสม
NBomber SignalR Load Test
// === NBomber SignalR Load Test สำหรับ Blazor Server ===
// dotnet add package NBomber
// dotnet add package NBomber.Http
// dotnet add package Microsoft.AspNetCore.SignalR.Client
using NBomber.CSharp;
using NBomber.Contracts;
using Microsoft.AspNetCore.SignalR.Client;
using System.Diagnostics;
// === 1. SignalR Connection Test ===
var signalrScenario = Scenario.Create("signalr_connections", async context =>
{
var connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/_blazor")
.WithAutomaticReconnect()
.Build();
var sw = Stopwatch.StartNew();
try
{
await connection.StartAsync();
sw.Stop();
// จำลองการใช้งาน UI
await Task.Delay(Random.Shared.Next(1000, 5000));
// ส่ง Event (เหมือน User คลิก)
// await connection.InvokeAsync("DispatchBrowserEvent", "click", "{}");
await connection.StopAsync();
await connection.DisposeAsync();
return Response.Ok(statusCode: "200",
sizeBytes: 0,
latencyMs: sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
sw.Stop();
return Response.Fail(statusCode: "500",
message: ex.Message,
latencyMs: sw.ElapsedMilliseconds);
}
})
.WithWarmUpDuration(TimeSpan.FromSeconds(10))
.WithLoadSimulations(
// Ramp up to 100 concurrent connections
Simulation.RampingInject(rate: 10, interval: TimeSpan.FromSeconds(1),
during: TimeSpan.FromMinutes(1)),
// Sustain 100 connections for 5 minutes
Simulation.Inject(rate: 100, interval: TimeSpan.FromSeconds(1),
during: TimeSpan.FromMinutes(5)),
// Ramp down
Simulation.RampingInject(rate: 10, interval: TimeSpan.FromSeconds(1),
during: TimeSpan.FromSeconds(30))
);
// === 2. API Load Test ===
var apiScenario = Scenario.Create("api_load", async context =>
{
using var client = new HttpClient();
var sw = Stopwatch.StartNew();
try
{
var response = await client.GetAsync("https://localhost:5001/api/users");
sw.Stop();
return response.IsSuccessStatusCode
? Response.Ok(statusCode: ((int)response.StatusCode).ToString(),
sizeBytes: (int)(response.Content.Headers.ContentLength ?? 0),
latencyMs: sw.ElapsedMilliseconds)
: Response.Fail(statusCode: ((int)response.StatusCode).ToString(),
latencyMs: sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
sw.Stop();
return Response.Fail(message: ex.Message, latencyMs: sw.ElapsedMilliseconds);
}
})
.WithLoadSimulations(
Simulation.Inject(rate: 50, interval: TimeSpan.FromSeconds(1),
during: TimeSpan.FromMinutes(3))
);
// รัน
NBomberRunner
.RegisterScenarios(signalrScenario, apiScenario)
.WithReportFormats(ReportFormat.Html, ReportFormat.Csv)
.Run();
// dotnet run
k6 API Load Test
// === k6 Load Test สำหรับ Blazor API ===
// k6 run blazor-load-test.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';
import ws from 'k6/ws';
const errorRate = new Rate('error_rate');
const apiLatency = new Trend('api_latency');
export const options = {
scenarios: {
// API Load Test
api_test: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 200 },
{ duration: '1m', target: 0 },
],
},
// WebSocket Stress Test
ws_test: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 100 },
{ duration: '5m', target: 500 },
{ duration: '1m', target: 0 },
],
startTime: '5m',
},
},
thresholds: {
http_req_duration: ['p(95)<500'],
error_rate: ['rate<0.05'],
http_req_failed: ['rate<0.01'],
},
};
const BASE_URL = __ENV.API_URL || 'https://localhost:5001';
export default function () {
group('Blazor API Endpoints', () => {
// GET Users List
const usersRes = http.get(`/api/users`, {
headers: { 'Accept': 'application/json' },
});
check(usersRes, {
'users status 200': (r) => r.status === 200,
'users response < 500ms': (r) => r.timings.duration < 500,
});
errorRate.add(usersRes.status !== 200);
apiLatency.add(usersRes.timings.duration);
// GET Single User
const userId = Math.floor(Math.random() * 100) + 1;
const userRes = http.get(`/api/users/`);
check(userRes, {
'user status 200': (r) => r.status === 200,
});
// POST Create Order
const orderPayload = JSON.stringify({
customerId: userId,
items: [{ productId: 1, quantity: 2 }],
});
const orderRes = http.post(`/api/orders`, orderPayload, {
headers: { 'Content-Type': 'application/json' },
});
check(orderRes, {
'order created': (r) => r.status === 201 || r.status === 200,
});
});
sleep(1);
}
// k6 run --vus 100 --duration 5m blazor-load-test.js
// k6 run -e API_URL=https://staging.example.com blazor-load-test.js
Performance Optimization
// === Blazor Performance Optimization ===
// 1. Virtualize Component สำหรับ Large Lists
// @page "/large-list"
//
// <Virtualize Items="items" Context="item" ItemSize="50">
// <ItemContent>
// <div class="item-row">
// <span>@item.Name</span>
// <span>@item.Value</span>
// </div>
// </ItemContent>
// <Placeholder>
// <div class="placeholder-row">Loading...</div>
// </Placeholder>
// </Virtualize>
// 2. ShouldRender Override
// @code {
// private string _lastValue;
//
// protected override bool ShouldRender()
// {
// if (CurrentValue == _lastValue) return false;
// _lastValue = CurrentValue;
// return true;
// }
// }
// 3. IMemoryCache สำหรับ Server
// services.AddMemoryCache();
//
// public class UserService
// {
// private readonly IMemoryCache _cache;
//
// public async Task<List<User>> GetUsersAsync()
// {
// return await _cache.GetOrCreateAsync("users", async entry =>
// {
// entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
// return await _dbContext.Users.ToListAsync();
// });
// }
// }
// 4. Lazy Assembly Loading (WASM)
// <Router AppAssembly="typeof(App).Assembly"
// AdditionalAssemblies="lazyAssemblies"
// OnNavigateAsync="OnNavigateAsync">
// </Router>
//
// @code {
// private List<Assembly> lazyAssemblies = new();
//
// private async Task OnNavigateAsync(NavigationContext args)
// {
// if (args.Path == "admin")
// {
// var assemblies = await LazyAssemblyLoader
// .LoadAssembliesAsync(new[] { "AdminModule.wasm" });
// lazyAssemblies.AddRange(assemblies);
// }
// }
// }
// 5. Performance Monitoring Middleware
// app.Use(async (context, next) =>
// {
// var sw = Stopwatch.StartNew();
// context.Response.OnStarting(() =>
// {
// sw.Stop();
// context.Response.Headers["X-Response-Time"] = $"{sw.ElapsedMilliseconds}ms";
// return Task.CompletedTask;
// });
// await next();
// });
// Performance Checklist
var checklist = new Dictionary<string, bool>
{
["Virtualize large lists"] = true,
["ShouldRender optimization"] = true,
["IMemoryCache for DB queries"] = true,
["Lazy assembly loading (WASM)"] = true,
["Pre-rendering enabled"] = true,
["Response compression"] = true,
["SignalR message size limit"] = true,
["CDN for static files"] = true,
};
Console.WriteLine("Performance Checklist:");
foreach (var (item, done) in checklist)
{
var status = done ? "OK" : "TODO";
Console.WriteLine($" [{status}] {item}");
}
Best Practices
- SignalR Limits: ทดสอบ Max Concurrent Connections ก่อน Go-live ตั้ง Connection Limits
- Virtualize: ใช้ Virtualize Component สำหรับ List ที่มีมากกว่า 100 Items
- Caching: ใช้ IMemoryCache ลด Database Queries สำหรับข้อมูลที่ไม่เปลี่ยนบ่อย
- Lazy Loading: ใช้ Lazy Assembly Loading สำหรับ WASM ลด Initial Bundle Size
- Pre-rendering: เปิด Pre-rendering สำหรับ SEO และ First Contentful Paint
- Compression: เปิด Response Compression สำหรับ SignalR Messages
Blazor Load Testing ทำอย่างไร
Blazor Server ทดสอบ SignalR WebSocket และ HTTP ใช้ NBomber SignalR k6 API Playwright Browser วัด Response Time Concurrent Connections Memory CPU
SignalR รับ Concurrent Connections ได้เท่าไร
Single Server 5,000-20,000 ขึ้นอยู่กับ CPU Memory Bandwidth Azure SignalR Service Scale 100,000+ ต้อง Load Test หา Limits จริง
Blazor WASM ต้อง Load Test อะไร
ทดสอบ Backend API ที่ WASM เรียก Initial Load Time WASM Download Size API Response Time Concurrent API Requests CDN Performance Static Files
วิธี Optimize Performance ของ Blazor ทำอย่างไร
Server ลด Re-renders ShouldRender() Virtualize List IMemoryCache ลด SignalR Payload WASM Lazy Loading AOT Compilation Trim assemblies ลด Bundle Pre-rendering
สรุป
Blazor Load Testing ครอบคลุม SignalR Connections API Endpoints Browser Rendering ใช้ NBomber สำหรับ SignalR k6 สำหรับ API Playwright สำหรับ E2E Optimize ด้วย Virtualize Caching Lazy Loading Pre-rendering Compression ทดสอบก่อน Go-live
