Files
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

562 lines
12 KiB
Markdown

# Blazor Performance & Advanced Patterns
## Rendering Optimization
### ShouldRender Override
Control when components re-render to prevent unnecessary rendering cycles.
```csharp
@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
```csharp
@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
```csharp
@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
```csharp
@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
```csharp
@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)
```csharp
@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 #
```csharp
@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");
}
}
```
### JS Module Isolation (Recommended)
```csharp
// 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
```csharp
// 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
```csharp
@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
```csharp
<!-- 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
```xml
<!-- .csproj -->
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
```
Benefits:
- No JIT compilation at runtime
- Faster startup time
- ~20% larger download
- Production recommended
### Trimming
```xml
<!-- .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
```xml
<!-- .csproj -->
<PropertyGroup>
<BlazorWebAssemblyEnableCompression>true</BlazorWebAssemblyEnableCompression>
</PropertyGroup>
```
Server-side (in Program.cs):
```csharp
app.UseResponseCompression();
builder.Services.AddResponseCompression(opts =>
{
opts.Filters.Add(new GzipCompressionProvider());
opts.Filters.Add(new BrotliCompressionProvider());
});
```
### Minimize JavaScript Interop
```csharp
// 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.
```csharp
@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.
```html
<!-- 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](components-lifecycle.md) for component disposal patterns. See [state-management-events.md](state-management-events.md) for state update optimization.