Blazor WebAssembly 성능 최적화
Blazor WebAssembly는 C#을 브라우저에서 실행하는 강력한 프레임워크지만, 초기 로딩 크기와 렌더링 성능이 주요 최적화 대상입니다. .NET 8에서는 AOT 컴파일, Static SSR, Streaming 렌더링이 추가되어 성능이 크게 개선되었습니다.
1. 번들 크기 최소화
섹션 제목: “1. 번들 크기 최소화”<PropertyGroup> <!-- IL Trimming: 사용하지 않는 코드 제거 --> <PublishTrimmed>true</PublishTrimmed> <TrimMode>full</TrimMode>
<!-- AOT 컴파일: WASM 네이티브 코드로 변환 --> <RunAOTCompilation>true</RunAOTCompilation>
<!-- 압축 --> <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport> <InvariantGlobalization>true</InvariantGlobalization></PropertyGroup># 게시 빌드 (최적화 최대)dotnet publish -c Release
# AOT 포함 (빌드 오래 걸리나 런타임 빠름)dotnet publish -c Release -p:RunAOTCompilation=true2. Lazy Loading — 어셈블리 지연 로드
섹션 제목: “2. Lazy Loading — 어셈블리 지연 로드”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); } }}3. 렌더링 최적화 — ShouldRender
섹션 제목: “3. 렌더링 최적화 — ShouldRender”@* 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; }}4. Virtualization — 대용량 목록
섹션 제목: “4. Virtualization — 대용량 목록”@* 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); }}5. JS Interop 최적화
섹션 제목: “5. JS Interop 최적화”// ❌ 느림: 매 호출마다 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 SSR7. 캐싱 전략
섹션 제목: “7. 캐싱 전략”// 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; }}8. PWA 지원 — 오프라인 캐싱
섹션 제목: “8. PWA 지원 — 오프라인 캐싱”// wwwroot/service-worker-assets.js (자동 생성)// wwwroot/service-worker.js (커스터마이징 가능)
// manifest.webmanifest{ "name": "My Blazor App", "short_name": "BlazorApp", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#03173d", "icons": [ { "src": "icon-192.png", "type": "image/png", "sizes": "192x192" }, { "src": "icon-512.png", "type": "image/png", "sizes": "512x512" } ]}// .csproj에 PWA 활성화// <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
// 오프라인 감지@inject NavigationManager Navigation@inject IJSRuntime JS
@code { private bool _isOnline = true;
protected override async Task OnInitializedAsync() { _isOnline = await JS.InvokeAsync<bool>("navigator.onLine.toString") != "false"; }}9. 번들 크기 측정
섹션 제목: “9. 번들 크기 측정”# 게시 후 번들 크기 확인dotnet publish -c Release -o out
# 압축 전후 크기 (대략적인 기준)# 최소 Blazor WASM: ~2MB (Brotli 압축 후 ~600KB)# AOT 포함: ~8MB (압축 후 ~2MB)
# Trimming 효과 확인dotnet publish -c Release -p:PublishTrimmed=true -p:TrimMode=full# 출력: Linking: size reduced by X% (Y → Z bytes)Blazor WASM 최적화의 핵심은 번들 크기(Trimming + AOT), 렌더링 횟수(ShouldRender), 목록 성능(Virtualize), JS Interop 비용(모듈 캐시) 네 가지입니다. .NET 8에서는 Static SSR과 WebAssembly 렌더링을 페이지별로 혼용해 초기 로딩은 서버 렌더로 빠르게 하고, 인터랙티브 부분만 WASM으로 처리하세요. PWA로 오프라인 지원과 설치 가능한 앱 경험을 추가하고, dotnet publish -p:PublishTrimmed=true로 번들 크기를 주기적으로 측정하세요.