콘텐츠로 이동

Blazor WebAssembly 성능 최적화

Blazor WebAssembly는 C#을 브라우저에서 실행하는 강력한 프레임워크지만, 초기 로딩 크기와 렌더링 성능이 주요 최적화 대상입니다. .NET 8에서는 AOT 컴파일, Static SSR, Streaming 렌더링이 추가되어 성능이 크게 개선되었습니다.


.csproj
<PropertyGroup>
<!-- IL Trimming: 사용하지 않는 코드 제거 -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<!-- AOT 컴파일: WASM 네이티브 코드로 변환 -->
<RunAOTCompilation>true</RunAOTCompilation>
<!-- 압축 -->
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
Terminal window
# 게시 빌드 (최적화 최대)
dotnet publish -c Release
# AOT 포함 (빌드 오래 걸리나 런타임 빠름)
dotnet publish -c Release -p:RunAOTCompilation=true

2. Lazy Loading — 어셈블리 지연 로드

섹션 제목: “2. Lazy Loading — 어셈블리 지연 로드”
Program.cs
builder.Services.AddScoped<LazyAssemblyLoader>();
// 라우팅에서 Lazy 어셈블리 지정
// App.razor
<Router AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="@lazyLoadedAssemblies">
...
</Router>
@code {
private List<Assembly> lazyLoadedAssemblies = new();
protected override async Task OnInitializedAsync()
{
// 특정 경로 진입 시에만 어셈블리 로드
Router.OnNavigateAsync = NavigationHandler;
}
private async Task NavigationHandler(NavigationContext ctx)
{
if (ctx.Path.StartsWith("/admin"))
{
var assemblies = await LazyLoader.LoadAssembliesAsync(
new[] { "AdminModule.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
}
}

@* CounterDisplay.razor *@
@inherits ComponentBase
<p>Count: @Count</p>
@code {
[Parameter] public int Count { get; set; }
private int _lastRenderedCount = -1;
// 실제로 변경된 경우에만 다시 렌더링
protected override bool ShouldRender()
{
if (_lastRenderedCount == Count) return false;
_lastRenderedCount = Count;
return true;
}
}

@* 10만 건도 부드럽게 렌더링 *@
<div style="height: 500px; overflow-y: auto;">
<Virtualize Items="@AllItems"
Context="item"
OverscanCount="3">
<ItemContent>
<div class="item-row">
<span>@item.Id</span>
<span>@item.Name</span>
</div>
</ItemContent>
<Placeholder>
<div class="loading-placeholder">로딩 중...</div>
</Placeholder>
</Virtualize>
</div>
@* 서버 페이지네이션과 결합 *@
<Virtualize Context="item"
ItemsProvider="@LoadItems"
ItemSize="50">
<div>@item.Name</div>
</Virtualize>
@code {
private async ValueTask<ItemsProviderResult<Item>> LoadItems(
ItemsProviderRequest req)
{
var result = await Api.GetPageAsync(req.StartIndex, req.Count);
return new(result.Items, result.TotalCount);
}
}

// ❌ 느림: 매 호출마다 JS 평가
await JSRuntime.InvokeVoidAsync("console.log", message);
// ✅ 빠름: 모듈 사전 로드 + IJSObjectReference 캐시
public class JsInteropService : IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> _module;
public JsInteropService(IJSRuntime js)
{
_module = new(() => js.InvokeAsync<IJSObjectReference>(
"import", "./js/myModule.js").AsTask());
}
public async ValueTask LogAsync(string msg)
{
var m = await _module.Value;
await m.InvokeVoidAsync("log", msg);
}
public async ValueTask DisposeAsync()
{
if (_module.IsValueCreated)
await (await _module.Value).DisposeAsync();
}
}

6. .NET 8 Static SSR + Enhanced Navigation

섹션 제목: “6. .NET 8 Static SSR + Enhanced Navigation”
// Program.cs (.NET 8)
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
// App.razor
<Routes @rendermode="InteractiveWebAssembly" />
// 개별 페이지 렌더 모드 지정
@page "/counter"
@rendermode InteractiveWebAssembly
// 정적 페이지 (서버 렌더, JS 없음)
@page "/about"
// rendermode 없음 → Static SSR

// HttpClient 응답 캐싱
builder.Services.AddScoped(sp =>
{
var http = new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};
return http;
});
// 컴포넌트 수준 캐싱
@code {
private static readonly Dictionary<int, Product> _cache = new();
protected override async Task OnInitializedAsync()
{
if (!_cache.TryGetValue(Id, out var product))
{
product = await Http.GetFromJsonAsync<Product>($"api/products/{Id}");
_cache[Id] = product!;
}
Product = product;
}
}

Blazor WASM 최적화의 핵심은 번들 크기(Trimming + AOT), 렌더링 횟수(ShouldRender), 목록 성능(Virtualize), JS Interop 비용(모듈 캐시) 네 가지입니다. .NET 8에서는 Static SSR과 WebAssembly 렌더링을 페이지별로 혼용해 초기 로딩은 서버 렌더로 빠르게 하고, 인터랙티브 부분만 WASM으로 처리하세요.