Files
Nexus.Reader/.agent/skills/blazor-expert/resources/performance-advanced.md
T
Antigravity 5a2223a4c8 feat: Ingestion Pipeline Stabilization and WASM Service Proxies (#42)
This PR stabilizes the Nexus Ingestion Engine by implementing functional service proxies for the Blazor WASM client and refining the backend infrastructure for real-time progress tracking and database compatibility.

### Key Changes
- **Infrastructure Stabilization**:
  - Implemented production-grade `EbookRepository` with PostgreSQL `EF.Functions.ILike` support.
  - Enforced `IsReadyForReading = false` state for newly added ebooks (resolves #35).
  - Updated `SignalRSyncBroadcaster` to support targeted user messaging and ingestion-specific progress updates (resolves #37).
- **WASM Client Functional Proxies**:
  - Replaced "Throwing" dummy services with `WasmEbookRepository`, `WasmSyncBroadcaster`, `WasmBookStorageService`, and `WasmEmbeddingGenerator`.
  - These services proxy requests to the backend via a new set of Minimal API endpoints in `NexusReader.Web`.
- **Domain Refinement**:
  - Added `IsReadyForReading` flag to the `Ebook` entity to manage background AI processing states.

### Related Issues
- Fixes #35
- Fixes #36
- Fixes #37

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #42
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
2026-05-13 18:24:24 +00:00

12 KiB

Blazor Performance & Advanced Patterns

Rendering Optimization

ShouldRender Override

Control when components re-render to prevent unnecessary rendering cycles.

@page "/optimized"

<button @onclick="IncrementCount">Clicked @count times</button>
<ChildComponent Value="@value" />

@code {
    private int count = 0;
    private string value = "test";

    protected override bool ShouldRender()
    {
        // Only render if value changed, not if count changed
        // This component doesn't display count directly
        return false; // Skip render
    }

    private void IncrementCount()
    {
        count++;
        // Component won't re-render, child component won't re-render either
    }
}

Tracking Changed Fields

@page "/tracker"

<button @onclick="UpdateName">Update Name</button>
<button @onclick="UpdateAge">Update Age</button>

<p>Name: @name</p>
<p>Age: @age</p>

@code {
    private string? name;
    private int age;
    private bool nameChanged = false;
    private bool ageChanged = false;

    protected override bool ShouldRender()
    {
        if (!nameChanged && !ageChanged)
        {
            return false;
        }

        nameChanged = false;
        ageChanged = false;
        return true;
    }

    private void UpdateName()
    {
        name = "New Name";
        nameChanged = true;
    }

    private void UpdateAge()
    {
        age = 30;
        ageChanged = true;
    }
}

Key Directive for List Items

@page "/list"

<button @onclick="AddItem">Add Item</button>

@foreach (var item in items)
{
    <!-- WITHOUT @key - new ItemComponent created for each item -->
    <!-- <ItemComponent Item="@item" />-->

    <!-- WITH @key - same ItemComponent reused if item.Id stays in list -->
    <ItemComponent @key="item.Id" Item="@item" />
}

@code {
    private List<Item> items = [];

    private void AddItem()
    {
        items = items.Prepend(new Item { Id = Guid.NewGuid(), Name = "New" }).ToList();
    }
}

public class Item
{
    public Guid Id { get; set; }
    public string? Name { get; set; }
}

Why @key matters:

  • Helps Blazor's diffing algorithm match old components to new items
  • Prevents component state loss during list reordering
  • Improves performance with large lists

IDisposable for Cleanup

@implements IAsyncDisposable
@inject IJSRuntime JS

private IJSObjectReference? module;
private Timer? timer;

protected override async Task OnInitializedAsync()
{
    timer = new Timer(_ => UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        module = await JS.InvokeAsync<IJSObjectReference>(
            "import", "./myScript.js");
    }
}

async ValueTask IAsyncDisposable.DisposeAsync()
{
    timer?.Dispose();
    
    if (module is not null)
    {
        await module.DisposeAsync();
    }
}

Virtualization

Virtualize large lists to render only visible items.

Basic Virtualization

@page "/large-list"
@using Microsoft.AspNetCore.Components.Web.Virtualization

<Virtualize Items="@largeList" Context="item">
    <div class="item">
        <p>@item.Id - @item.Name</p>
    </div>
</Virtualize>

@code {
    private List<Item> largeList = [];

    protected override void OnInitialized()
    {
        // Generate 100,000 items
        largeList = Enumerable.Range(1, 100000)
            .Select(i => new Item { Id = i, Name = $"Item {i}" })
            .ToList();
    }
}

Async Virtualization (Infinite Scroll)

@page "/infinite-scroll"
@using Microsoft.AspNetCore.Components.Web.Virtualization

<Virtualize ItemsProvider="@LoadItems" Context="item" OverscanCount="5">
    <div>@item.Name</div>
</Virtualize>

@code {
    private async ValueTask<ItemsProviderResult<Item>> LoadItems(
        ItemsProviderRequest request)
    {
        // Simulate loading from server
        var startIndex = request.StartIndex;
        var count = request.Count;

        var items = await Service.GetItemsAsync(startIndex, count);

        // Return items and total count for scrollbar sizing
        return new ItemsProviderResult<Item>(items, totalItemCount: 1000000);
    }
}

Parameters:

  • Items - Static list of items to virtualize
  • ItemsProvider - Async method to load items on demand
  • OverscanCount - Extra items to render outside viewport (default 3)
  • ItemSize - Estimated height for scrollbar calculation

JavaScript Interop

Invoke JavaScript from C

@inject IJSRuntime JS

<button @onclick="CallJavaScript">Click me</button>

@code {
    private async Task CallJavaScript()
    {
        // Simple call - no return value
        await JS.InvokeVoidAsync("console.log", "Hello from Blazor");

        // With return value
        var result = await JS.InvokeAsync<string>("myFunction", arg1, arg2);

        // Generic call with any return type
        var data = await JS.InvokeAsync<Data>("loadData");
    }
}
// Component.razor
@implements IAsyncDisposable
@inject IJSRuntime JS

<div @ref="element">
    <canvas id="chart"></canvas>
</div>

@code {
    private ElementReference element;
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Import JS module
            module = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./scripts/chart.js");

            // Call exported function
            await module.InvokeVoidAsync("initChart", element);
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

/* scripts/chart.js */
export function initChart(element) {
    const canvas = element.querySelector('#chart');
    // Initialize chart library
}

Invoke C# from JavaScript

// Component.razor
@implements IAsyncDisposable
@inject IJSRuntime JS

<button @onclick="SetupInterop">Setup</button>

@code {
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./scripts/interop.js");

            // Pass C# object reference to JS
            var objRef = DotNetObjectReference.Create(this);
            await module.InvokeVoidAsync("setupInterop", objRef);
        }
    }

    [JSInvokable]
    public async Task HandleJSEvent(string data)
    {
        Console.WriteLine($"JS called C#: {data}");
        // Update component state
        StateHasChanged();
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

/* scripts/interop.js */
let dotnetHelper;

export function setupInterop(dotnetRef) {
    dotnetHelper = dotnetRef;
    
    // Call C# method from JS
    document.addEventListener('click', async (e) => {
        await dotnetHelper.invokeMethodAsync('HandleJSEvent', 'User clicked');
    });
}

Error Handling in Interop

@code {
    private async Task SafeInvokeAsync()
    {
        try
        {
            await JS.InvokeVoidAsync("riskyFunction");
        }
        catch (JSException jsEx)
        {
            Console.WriteLine($"JS error: {jsEx.Message}");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("JS call was cancelled");
        }
    }
}

Lazy Loading

Load assemblies and components on demand.

Lazy-Loaded Component Routes

<!-- App.razor -->
<Router AppAssembly="@typeof(App).Assembly"
        AdditionalAssemblies="@additionalAssemblies"
        OnNavigateAsync="@OnNavigateAsync">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <p>Loading...</p>
    </NotFound>
</Router>

@code {
    private List<Assembly>? additionalAssemblies;

    protected override async Task OnInitializedAsync()
    {
        additionalAssemblies = new();
    }

    private async Task OnNavigateAsync(NavigationContext context)
    {
        // Load admin assembly only when accessing /admin
        if (context.Path.StartsWith("admin"))
        {
            var adminAssembly = await JS.InvokeAsync<byte[]>(
                "fetch", "./_framework/admin.wasm");
            
            additionalAssemblies!.Add(Assembly.Load(adminAssembly));
        }
    }
}

WASM Performance Best Practices

AOT Compilation

<!-- .csproj -->
<PropertyGroup>
    <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

Benefits:

  • No JIT compilation at runtime
  • Faster startup time
  • ~20% larger download
  • Production recommended

Trimming

<!-- .csproj -->
<PropertyGroup>
    <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

Benefits:

  • Removes unused code
  • ~40% smaller download
  • May cause runtime errors if reflection-based code removed
  • Test thoroughly in Release build

Compression

<!-- .csproj -->
<PropertyGroup>
    <BlazorWebAssemblyEnableCompression>true</BlazorWebAssemblyEnableCompression>
</PropertyGroup>

Server-side (in Program.cs):

app.UseResponseCompression();

builder.Services.AddResponseCompression(opts =>
{
    opts.Filters.Add(new GzipCompressionProvider());
    opts.Filters.Add(new BrotliCompressionProvider());
});

Minimize JavaScript Interop

// INEFFICIENT - Many JS calls
for (int i = 0; i < 1000; i++)
{
    await JS.InvokeVoidAsync("updateUI", i);
}

// EFFICIENT - Single JS call with batch data
var updates = Enumerable.Range(0, 1000).ToList();
await JS.InvokeVoidAsync("updateUIBatch", updates);

Error Boundaries

Handle component errors gracefully.

@page "/error-demo"

<ErrorBoundary>
    <ChildContent>
        <ChildComponent />
    </ChildContent>
    <ErrorContent Context="ex">
        <div class="alert alert-danger">
            <h4>Error</h4>
            <p>@ex.Message</p>
            <button @onclick="ResetError">Try Again</button>
        </div>
    </ErrorContent>
</ErrorBoundary>

@code {
    private ErrorBoundary? errorBoundary;

    private async Task ResetError()
    {
        await errorBoundary!.RecoverAsync();
    }
}

CSS Isolation

Scope CSS to specific components.

<!-- MyComponent.razor -->
<div class="container">
    <h1>@Title</h1>
</div>

<!-- MyComponent.razor.css -->
.container {
    background-color: blue;
    padding: 20px;
}

h1 {
    color: white;
    font-size: 24px;
}

Benefits:

  • No global namespace pollution
  • Component-specific styling
  • CSS automatically scoped to component
  • Compiled into assembly

Best Practices Summary

Performance

  • Use @key on list items
  • Override ShouldRender() to prevent unnecessary renders
  • Use virtualization for large lists
  • Minimize JavaScript interop calls
  • Enable AOT compilation and trimming for WASM

JavaScript Interop

  • Use module isolation pattern
  • Always dispose JS module references
  • Handle JS exceptions properly
  • Only call JS in OnAfterRender with firstRender check
  • Minimize interop calls for performance

Architecture

  • Keep components simple and focused
  • Move logic to services
  • Use cascading values for shared state
  • Implement IDisposable for cleanup
  • Validate authorization on server side

User Experience

  • Show loading states during async operations
  • Provide error feedback
  • Use AuthorizeView for conditional rendering
  • Implement error boundaries
  • Test on slow connections

Related Resources: See components-lifecycle.md for component disposal patterns. See state-management-events.md for state update optimization.