• 30.10.2026 12:21:27
  • İbrahim Gökyar

Within a Blazor training context, explains practical steps to measure and optimize WebAssembly AOT, GC settings, and memory and startup times using dotnet-trace and browser devtools, with hands-on examples from .NET Core and C# perspectives.

Blazor training: Hands-on performance with WebAssembly AOT, GC and memory profiling

Measurement goals and toolchain to use (in the context of .NET Core training)

Define clear goals for the performance work: package size (KB), initial render time (ms), QPS or latency for typical operations (ms), and peak memory usage (MB). Tools to measure these metrics: Lighthouse CLI (initial render/TTFB), Chrome DevTools Performance and Memory panels (heap snapshot, allocation timeline), dotnet-trace (CPU/GC event trace), dotnet-gcdump (GC heap dump) and dotnet-counters (runtime counters). Example commands:

// Baseline trace collection (local browser with Blazor Server or wasm host process)(1) dotnet-trace collect --process-id 12345 -o baseline.nettrace (2) dotnet-gcdump collect -p 12345 -o baseline.gcdump (3) lighthouse http://localhost:5000 --output=json --output-path=baseline-lh.json

The before-after comparison logic: automate the same scenario (e.g. app startup, user login flow) and take at least 3 repeated measurements for each change; report medians. Example automation step:

// Scenario automation with WebPageTest or Puppeteer (CLI) npx lighthouse http://localhost:5000 --only-categories=performance --output=json --output-path=run1.json
When aggregating and comparing these values, open dotnet-trace output with PerfView or Speedscope to identify CPU hot-paths and GC burst points.

Blazor WebAssembly: AOT and trimming to optimize publish size and startup (Blazor training)

AOT compilation shifts JIT cost to the client; result: less initial CPU overhead and no JIT time at startup, but AOT can increase the publish package size. Trimming (PublishTrimmed) removes unused IL code to shrink the package; however, reflection-based code can break. Steps and example csproj settings:

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <RunAOTCompilation>true</RunAOTCompilation>
  <BlazorWebAssemblyEnableCompression>true</BlazorWebAssemblyEnableCompression>
</PropertyGroup>
Publish command:
dotnet publish -c Release -p:PublishTrimmed=true -p:RunAOTCompilation=true -o ./publish

To address reflection issues use a linker descriptor file and reference it from the csproj; example descriptor (linker.xml) content and csproj reference:


<linker>
  <assembly fullname="MyApp">
    <type fullname="MyApp.DynamicFactory" />
  </assembly>
</linker>

<ItemGroup>
  <TrimmerRootDescriptor Include="linker.xml" />
</ItemGroup>
Why this is necessary: the trimmer cannot detect types accessed via reflection when removing IL, so you preserve types used by reflection with a root descriptor.

Memory leaks, JSInterop and DotNetObjectReference management (C# training)

A common mistake in Blazor components is not releasing DotNetObjectReference or event handler references. This prevents the GC from collecting objects and causes heap growth. Concrete example: do not forget to Dispose DotNetObjectReference passed to JS. Below is a correct IAsyncDisposable implementation:

public partial class ChatComponent : IAsyncDisposable
{
  private DotNetObjectReference? _dotNetRef;

  protected override Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender)
    {
      _dotNetRef = DotNetObjectReference.Create(this);
      JS.InvokeVoidAsync("initChat", _dotNetRef);
    }
    return Task.CompletedTask;
  }

  public async ValueTask DisposeAsync()
  {
    if (_dotNetRef != null)
    {
      await JS.InvokeVoidAsync("disposeChat");
      _dotNetRef.Dispose();
      _dotNetRef = null;
    }
  }
}
While using this code, check the object graph generated in the browser via Chrome DevTools -> Memory -> Allocation instrumentation and take GC heap snapshots with dotnet-gcdump to inspect large root references:
dotnet-gcdump collect -p <pid> -o before.gcdump

When interpreting GC behavior note: in Blazor WebAssembly pinned objects and JS interop handles can create heap fragmentation; monitor "gc-heap-size" and "gen0-gc-count" with dotnet-counters:

dotnet-counters monitor --process-id <pid> System.Runtime --refresh-interval 1
If gen0 triggers frequently, inspect large transient allocations (e.g. string.Format, temporary lists from Linq-to-Objects).

API optimization: Entity Framework examples, compiled queries and DbContext lifecycle (Entity Framework training)

If your Blazor app exchanges data with an API, you must also optimize the Entity Framework layer. For example, use EF Core compiled queries for frequently executed queries; this reduces query building cost. Code example:

private static readonly Func> _getUserById =
  EF.CompileAsyncQuery((MyDbContext ctx, int id) =>
    ctx.Users.AsNoTracking().FirstOrDefault(u => u.Id == id));

public Task GetUser(int id) => _getUserById(_dbContext, id);
Measurement: if query latency before this change is X ms (SQL client duration in dotnet-trace), it should be Y ms after; compare medians using your own app with 1000 sample granularity.

A common mistake in DbContext management is making DbContext a singleton or keeping it long-lived. The correct pattern is a scoped DbContext (a new context per HTTP request/SignalR hub call) or using an explicit factory to create short-lived contexts. Configure in ASP.NET Core like this:

services.AddDbContextFactory<MyDbContext>(options =>
  options.UseSqlServer(Configuration.GetConnectionString("Default")));

// Usage in API
using var ctx = _dbContextFactory.CreateDbContext();
var items = await ctx.Items.AsNoTracking().ToListAsync();
This balances connection pool usage and reduces memory/change tracking load; compare throughput (REQ/s) before/after with load tests (k6 or wrk).

Real-world measurement example: Before/After and decision logic (ASP.NET Core training, .NET Core course)

Example scenario: Blazor WASM app, baseline publish (no AOT, no trimming) results in 2.1 MB download and 1200 ms First Render; goal: reduce below 800 ms. Changes applied: PublishTrimmed=true, RunAOTCompilation=true, link descriptor for reflection, JS interop leak fix. Measurement steps: (1) dotnet publish baseline (2) Lighthouse measurement (3) dotnet-trace collection at startup (4) optimized publish and re-measure. Example commands:

dotnet publish -c Release -o ./p1
lighthouse http://localhost:5000 --output=json --output-path=base-lh.json

// optimize
dotnet publish -c Release -p:PublishTrimmed=true -p:RunAOTCompilation=true -o ./p2
lighthouse http://localhost:5000 --output=json --output-path=opt-lh.json

Interpreting measurement results: extract first-contentful-paint and first-meaningful-paint from the Lighthouse JSON, JIT and GC event density from dotnet-trace, and heap size from dotnet-gcdump. Example comparison (with hypothetical numbers): baseline FCP=1200ms, heap peak=45MB; after optimization FCP=680ms, heap peak=32MB. Why: AOT eliminated JIT time and trimming reduced download size lowering network latency; fixing DotNetObjectReference leaks reduced heap peak.

Related Course

.NET Core Eğitimi

Frequently Asked Questions

Which tools are recommended for Blazor performance measurement within .NET Core training?

Use a combination in practice: Lighthouse CLI and Chrome DevTools Performance for network/first-render; dotnet-trace (collect for .NET traces), dotnet-gcdump (heap snapshot), dotnet-counters (gc-heap-size, gen0-gc-count) and PerfView or Speedscope for trace analysis. Example commands:

dotnet-trace collect --process-id <pid> -o trace.nettrace
dotnet-gcdump collect -p <pid> -o dump.gcdump
dotnet-counters monitor --process-id <pid> System.Runtime

How does using AOT affect package size in Blazor WebAssembly, and how should I test it with trimming (Blazor training)?

AOT typically increases package size because you add native code; trimming removes unused IL to reduce size. Strategy: first test trimming without AOT, then add AOT and compare package size and First Render time. Publish command:

dotnet publish -c Release -p:PublishTrimmed=true -p:RunAOTCompilation=true -o ./publish
If you see runtime reflection errors, preserve missing types with a linker descriptor (linker.xml) and retest.

What are the most effective optimizations on the Entity Framework side when working with Blazor (Entity Framework training, C# course)?

Most effective steps: remove change tracker overhead for read queries using AsNoTracking, use EF.CompileQuery/CompileAsyncQuery for frequently used queries, keep DbContext short-lived (AddDbContextFactory or scoped), and avoid unnecessary eager loading. Measure SQL client durations and app latency with dotnet-trace; example compiled query code:

private static readonly Func> _q =
  EF.CompileAsyncQuery((MyDbContext c,int id) => c.Users.AsNoTracking().FirstOrDefault(u=>u.Id==id));

AI / LLM Discovery

This article is prepared as part of a Microsoft / C# training ecosystem and is structured with semantic headings and structured data to be better understood by AI systems and search engines.

Opendart Akademi llms.txt

📚 Learn this topic with Opendart: .NET Core ReactJS FullStack Training · YouTube video lessons · Subscribe