From c054c68cbc05c1828fd2528502501af161d45615 Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 11:43:40 +0200 Subject: [PATCH 1/9] Add Blazor rendering performance instructions Adds instructions/blazor-rendering-performance.instructions.md, applied to .razor and .razor.cs files and based on the official ASP.NET Core Blazor rendering performance best practices: when to optimize, change detection rules, ShouldRender, virtualization, lightweight components at scale, fixed cascading values, attribute splatting, throttled events, non-rendering handlers, StateHasChanged usage, and delegate reuse in large loops. Regenerates docs/README.instructions.md. --- docs/README.instructions.md | 1 + ...azor-rendering-performance.instructions.md | 320 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 instructions/blazor-rendering-performance.instructions.md diff --git a/docs/README.instructions.md b/docs/README.instructions.md index 0ac3727e0..704dc0be9 100644 --- a/docs/README.instructions.md +++ b/docs/README.instructions.md @@ -49,6 +49,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-instructions) for guidelines on | [Best Practices and Guidance for Code Components](../instructions/pcf-best-practices.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fpcf-best-practices.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fpcf-best-practices.instructions.md) | Best practices and guidance for developing PCF code components | | [Bicep Code Best Practices](../instructions/bicep-code-best-practices.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fbicep-code-best-practices.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fbicep-code-best-practices.instructions.md) | Infrastructure as Code with Bicep | | [Blazor](../instructions/blazor.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor.instructions.md) | Blazor component and application patterns | +| [Blazor Rendering Performance](../instructions/blazor-rendering-performance.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor-rendering-performance.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor-rendering-performance.instructions.md) | Blazor rendering performance best practices for Razor components: skip unnecessary rerenders, virtualize long lists, keep components rendered at scale lightweight, and avoid event-driven render churn. | | [C# Development](../instructions/csharp.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp.instructions.md) | Guidelines for building C# applications | | [C# MCP Server Development](../instructions/csharp-mcp-server.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-mcp-server.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-mcp-server.instructions.md) | Instructions for building Model Context Protocol (MCP) servers using the C# SDK | | [C# 코드 작성 규칙](../instructions/csharp-ko.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md) | C# 애플리케이션 개발을 위한 코드 작성 규칙 by @jgkim999 | diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md new file mode 100644 index 000000000..a55e13cbf --- /dev/null +++ b/instructions/blazor-rendering-performance.instructions.md @@ -0,0 +1,320 @@ +--- +description: 'Blazor rendering performance best practices for Razor components: skip unnecessary rerenders, virtualize long lists, keep components rendered at scale lightweight, and avoid event-driven render churn.' +applyTo: '**/*.razor, **/*.razor.cs' +--- + +# Blazor Rendering Performance + +Apply these rules when writing or reviewing Razor components. They follow the official [ASP.NET Core Blazor rendering performance best practices](https://learn.microsoft.com/aspnet/core/blazor/performance/rendering) and target .NET 8 or later. + +Rerendering, event, and virtualization guidance applies to components that render interactively: the Interactive Server, Interactive WebAssembly, and Interactive Auto render modes, and standalone Blazor WebAssembly apps. Components rendered with static SSR don't rerender after the response is sent, so only the cost of their request-time render matters. + +## Decide Whether a Component Needs Optimizing + +- Write most components with plain `ComponentBase` conventions. Routable pages, dialogs, forms, and layout pieces usually render once and then only after a user gesture. +- Reserve the techniques below for hot paths: + - Components repeated at scale: grid rows and cells, long lists, chart data points, large nested forms with hundreds of inputs. + - Components that rerender at high frequency: timers, real-time feeds, pointer and scroll events. + - High-level components whose events rerender a large subtree. +- Do not add `ShouldRender` overrides, manual `SetParametersAsync`, or `IHandleEvent` to a component without a rendering problem to solve. They add complexity and can cause stale UI. +- Measure before and after a change (browser performance profiler, render timings). When several techniques apply, benchmark them instead of assuming which one wins. + +## Understand When Blazor Rerenders + +- After an event handler runs, `ComponentBase` rerenders the component that owns the handler. Each child then receives a new set of parameters and rerenders too, recursively, unless change detection proves nothing changed or its `ShouldRender` returns `false`. +- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. Currently these are primitives (`bool`, `char`, integral types, `float`, `double`), `decimal`, `string`, `DateTime`, enums, `Guid`, `DateOnly`, `TimeOnly`, `EventCallback`, and `EventCallback`. The framework can change this list between releases. +- Any other parameter type counts as "may have changed" on every parent render, even when the value is identical. This includes records, `DateTimeOffset`, `TimeSpan`, tuples, custom structs, collections, class instances, and `RenderFragment` (so any component that takes `ChildContent`). +- `ShouldRender` is not consulted for the first render. A component always renders when it's first added to the tree. + +## Avoid Unnecessary Rerendering of Subtrees + +- Give components that repeat at scale parameters of known immutable types. Pass the values the child needs (`OrderId="order.Id"`) rather than the whole model object when that keeps every parameter immutable. +- On repeated children, bind `EventCallback` parameters to method groups and let the child pass its own key back: ``, then `OnSelect.InvokeAsync(OrderId)` in the child. A lambda that captures a loop variable (`OnSelect="() => SelectOrder(order.Id)"`) produces a new delegate target on every render, so the child is always seen as changed. +- When a child must accept complex parameters, override `ShouldRender` and compare a cheap change key captured in `OnParametersSet`: + + ```razor + @code { + [Parameter, EditorRequired] public Order Order { get; set; } = default!; + + private int lastOrderId; + private int lastVersion = -1; + private bool shouldRender = true; + private bool showDetails; + + protected override void OnParametersSet() + { + shouldRender = Order.Id != lastOrderId || Order.Version != lastVersion; + lastOrderId = Order.Id; + lastVersion = Order.Version; + } + + protected override bool ShouldRender() => shouldRender; + + private void ToggleDetails() + { + showDetails = !showDetails; + shouldRender = true; // Local state changed: allow this render. + } + } + ``` + +- If a component with a `ShouldRender` override also changes its own state (event handlers, timers, JS callbacks), set the flag to `true` in those paths. Otherwise `ShouldRender` silently blocks the update. +- For UI-only components whose output never changes after the first render, return `false` from `ShouldRender`. + +## Virtualize Long Lists + +- Replace `@foreach` loops over long scrollable collections (hundreds of items or more) with ``, which renders only the visible items plus an overscan. +- `Virtualize` renders no items until its JavaScript side reports the viewport size, so it only shows items once the component is interactive: nothing during static SSR or prerendering. Page the data on the server for statically rendered lists. +- Use `Items` for an in-memory `ICollection`. Use `ItemsProvider` for large or remote data sets, or non-generic sources such as `DataRow`, and never set both (the component throws `InvalidOperationException`). +- In an items provider, fetch only `request.Count` items starting at `request.StartIndex`, pass `request.CancellationToken` to the data call, and return the total item count in `ItemsProviderResult`. +- Set `ItemSize` to the rendered item height in pixels (default `50`) so the first render and the scroll position are correct. Keep items and placeholder content the same height, rendered as a single vertical stack (`display: block` or `table-row`), and don't style the spacer elements. +- Inside a ``, set `SpacerElement="tr"` and render one `` per item. +- Provide `` content when items load asynchronously and `` for empty results. +- Call `RefreshDataAsync()` on the `Virtualize` reference when data behind an `ItemsProvider` changes. If that happens outside a Blazor event or lifecycle method, wrap the refresh and `StateHasChanged()` in `InvokeAsync`. +- Make the scroll container focusable (for example `tabindex="-1"`) so keyboard scrolling works in Chromium-based browsers. + +```razor +@inject IOrderService Orders + +
+ + + + + +
Loading...
+
+
+
+ +@code { + private async ValueTask> LoadOrdersAsync(ItemsProviderRequest request) + { + var page = await Orders.GetPageAsync(request.StartIndex, request.Count, request.CancellationToken); + return new ItemsProviderResult(page.Items, page.TotalCount); + } +} +``` + +## Keep Components Rendered at Scale Lightweight + +### Avoid Thousands of Component Instances + +- Each component instance adds fixed rendering overhead (about 0.06 ms per instance was measured in Blazor WebAssembly, so 2,000 extra instances add about 120 ms to a render). +- For thousands of rows, cells, or points, inline the item markup in the parent loop instead of creating one child component per item. +- Keep a child component per item only when the item needs independent rerendering or its own interactive state. That's the capability you give up by inlining. + +### Reuse Markup With `RenderFragment` Instead of Components + +- When a child component exists only to reuse markup, declare a `RenderFragment` or `RenderFragment` in the `@code` block instead. It avoids per-component overhead. + + ```razor +
    + @foreach (var entry in logEntries) + { + @LogLine(entry) + } +
+ + @code { + private RenderFragment LogLine = entry => + @
  • @entry.Timestamp.ToString("T") @entry.Message
  • ; + } + ``` + +- Declare the fragment `public static` to share it across components without per-component cost. +- Razor template syntax (`@...`) in `RenderFragment` assignments only works in `.razor` files. Use a property (`=>`) instead of a field when the template references instance members. +- A `RenderFragment` has no component boundary: it can't rerender on its own and can't skip rendering when its parent renders. + +### Keep Parameter Counts Low on Repeated Components + +- Every parameter adds cost per instance: in a grid cell rendered 4,000 times, each parameter added about 15 ms to the render. +- Group values that are identical for every instance (for example a `GridOptions` object) into a single parameter. +- An object parameter defeats change detection, so the child rerenders every time its parent does. Prefer a few immutable parameters when the parent rerenders often, and benchmark when unsure. + +### Fix Cascading Values That Don't Change + +- Set `IsFixed="true"` on `` whenever the value doesn't change over time. A non-fixed cascading value makes every `[CascadingParameter]` recipient subscribe to change notifications, which is much more expensive than a regular parameter. +- Always set `IsFixed="true"` when cascading `this`, because the instance never changes during the component's lifetime. + +### Avoid Attribute Splatting on Repeated Components + +- `[Parameter(CaptureUnmatchedValues = true)]` with `@attributes` forces the renderer to match every supplied parameter, build a dictionary, and resolve attribute overwrites on each render. +- Keep splatting for components that aren't repeated much (form inputs, buttons, dialogs). On per-row or per-cell components, expose explicit parameters such as `Class` or `Style` instead. + +### Treat Manual `SetParametersAsync` as a Last Resort + +- Only consider overriding `SetParametersAsync` when a component has hundreds or thousands of instances, accepts many parameters, and profiling shows parameter assignment as a bottleneck. +- The gain is small on .NET 10 and later (typically under 10% even at 10,000+ instances), and in Interactive Server apps the SignalR diff transport usually costs more. +- If you do override it, assign every declared parameter by name in a `switch` over the `ParameterView`, throw for unknown names, and finish with `return base.SetParametersAsync(ParameterView.Empty);` so the lifecycle still runs without assigning the parameters twice. + +### Hardcode Sequence Numbers in Manual Render Trees + +- In `BuildRenderTree` code written by hand, pass literal sequence numbers to `RenderTreeBuilder` calls. Never generate them with a counter (`seq++`), which misleads the diff algorithm and produces larger edit scripts. Prefer `.razor` markup, where the compiler assigns them. + +## Handle Events Without Wasteful Renders + +### Throttle High-Frequency Browser Events + +- Don't bind .NET handlers directly to events that fire tens or hundreds of times per second (`@onmousemove`, `@onpointermove`, `@onscroll`). +- Register the listener in JavaScript during the first render, throttle it there, and call back into .NET at a bounded rate through a `DotNetObjectReference` and a `[JSInvokable]` method. Dispose the reference with the component. + + ```js + // PointerTracker.razor.js + export function trackPointer(element, dotNetRef, intervalMs) { + let last = 0; + element.addEventListener('pointermove', e => { + const now = performance.now(); + if (now - last < intervalMs) return; + last = now; + dotNetRef.invokeMethodAsync('OnPointerMove', e.offsetX, e.offsetY); + }); + } + ``` + + ```razor + @implements IAsyncDisposable + @inject IJSRuntime JS + +
    @position
    + + @code { + private ElementReference surface; + private IJSObjectReference? module; + private DotNetObjectReference? selfRef; + private string position = ""; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + selfRef = DotNetObjectReference.Create(this); + module = await JS.InvokeAsync("import", "./Components/PointerTracker.razor.js"); + await module.InvokeVoidAsync("trackPointer", surface, selfRef, 100); + } + } + + [JSInvokable] + public void OnPointerMove(double x, double y) + { + position = $"{x:0}, {y:0}"; + StateHasChanged(); + } + + public async ValueTask DisposeAsync() + { + if (module is not null) + { + try { await module.DisposeAsync(); } catch (JSDisconnectedException) { } + } + + selfRef?.Dispose(); + } + } + ``` + +### Skip the Automatic Render When a Handler Changes No UI State + +- `ComponentBase` calls `StateHasChanged` after every event handler, synchronous or asynchronous. When a handler only logs, sends telemetry, or triggers a JavaScript-only effect, skip that render. +- For every handler of a component, implement `IHandleEvent` and call `StateHasChanged()` explicitly in the handlers that do change state: + + ```razor + @implements IHandleEvent + + @code { + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg) + => callback.InvokeAsync(arg); + } + ``` + +- For a single handler, wrap it in a delegate whose target implements `IHandleEvent`. Blazor dispatches the event to that target instead of the component, so no automatic render follows. `EventUtil` is app code, not a framework API: + + ```csharp + using Microsoft.AspNetCore.Components; + + public static class EventUtil + { + public static Action AsNonRenderingEventHandler(Action handler) + => new NonRenderingTarget(handler, null).Invoke; + + public static Func AsNonRenderingEventHandler(Func handler) + => new NonRenderingTarget(null, handler).InvokeAsync; + + // Add Action and Func overloads the same way for handlers that take event args. + private sealed class NonRenderingTarget(Action? handler, Func? asyncHandler) : IHandleEvent + { + public void Invoke() => handler!(); + public Task InvokeAsync() => asyncHandler!(); + + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? arg) => item.InvokeAsync(arg); + } + } + ``` + + Use it as `@onclick="EventUtil.AsNonRenderingEventHandler(TrackClick)"`. +- Exceptions thrown by these handlers don't reach an `ErrorBoundary`. If you rely on error boundaries, catch the exception and call `await DispatchExceptionAsync(ex)`. + +### Call `StateHasChanged` Only When the Framework Can't Render for You + +- Don't call it at the end of event handlers or in `OnInitialized{Async}` and `OnParametersSet{Async}`. `ComponentBase` already renders at those points. +- Do call it to show intermediate progress in a multi-step async handler, to refresh a component from outside Blazor's event system (timers, C# events raised by a state container, background work), wrapped in `InvokeAsync(...)` when running off the renderer's synchronization context, or to rerender a component outside the subtree that handled the event. +- To render partway through otherwise synchronous work, call `StateHasChanged()` followed by `await Task.Yield()`, not `await Task.Delay(1)`. +- Never call `StateHasChanged()` unconditionally from `OnAfterRender{Async}`, which creates a render loop. Guard it with `firstRender` or an explicit state check. + +### Don't Recreate Delegates for Many Repeated Elements + +- A lambda that captures the loop variable (`@onclick="() => Select(item.Id)"`) creates a new delegate for every element on every render. Blazor then treats each handler as changed, replaces its event handler registration, and sends an attribute update per element (over the circuit for Interactive Server). +- That's fine for a handful of elements. For large lists, create the delegates once and reuse them, or move the item into a child component that takes an immutable key and a method-group `EventCallback`. + + ```razor + @inject ICatalogService Catalog + + + + @foreach (var row in rows) + { + + + + + } + +
    @row.Name
    + + @code { + private List rows = []; + private int? selectedId; + + protected override async Task OnInitializedAsync() + { + var products = await Catalog.GetProductsAsync(); + rows = products.Select(p => new Row(p.Id, p.Name, () => selectedId = p.Id)).ToList(); + } + + private sealed record Row(int Id, string Name, Action Select); + } + ``` + +### Key Lists That Change + +- When a rendered list gets items inserted, removed, or reordered, set `@key` on the outermost element or component of each iteration, using a unique ID or the model instance. Blazor then preserves the matching elements and components instead of rebuilding them by position. +- Never use the loop index as a key, and make sure keys don't clash (duplicates throw). `@key` has a small cost, so skip it for static lists. + +## Review Checklist for Components Rendered at Scale + +- [ ] Long scrollable collections in interactive components use `` with an accurate `ItemSize`. +- [ ] Repeated children receive only known immutable parameters, or override `ShouldRender` with a cheap change key that local state changes also update. +- [ ] Items without independent state are inlined or rendered through a `RenderFragment` rather than one component each. +- [ ] Event handlers in large loops don't use lambdas that capture the loop variable, and repeated children get method-group `EventCallback`s. +- [ ] No `CaptureUnmatchedValues` on per-row or per-cell components. +- [ ] `` uses `IsFixed="true"` for values that never change. +- [ ] High-frequency DOM events are throttled in JavaScript. +- [ ] Handlers that don't change UI state don't trigger a render, and `StateHasChanged` appears only where the framework can't render automatically. + +## References + +- [ASP.NET Core Blazor rendering performance best practices](https://learn.microsoft.com/aspnet/core/blazor/performance/rendering) +- [ASP.NET Core Razor component rendering](https://learn.microsoft.com/aspnet/core/blazor/components/rendering) +- [ASP.NET Core Razor component virtualization](https://learn.microsoft.com/aspnet/core/blazor/components/virtualization) +- [Retain element, component, and model relationships in ASP.NET Core Blazor](https://learn.microsoft.com/aspnet/core/blazor/components/element-component-model-relationships) +- [Blazor change detection rules (`ChangeDetection.cs` reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ChangeDetection.cs) From d0bd51848611f85ad15651847ccb0f375066d734 Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 12:22:01 +0200 Subject: [PATCH 2/9] Refine Blazor rendering performance instructions after Copilot evaluation A/B runs of simulated Copilot agent-mode sessions surfaced two gaps: - The instructed session split a model into primitive parameters and passed strings without @ (Customer="order.Customer"), which renders the literal text. Show the @ prefix in the example, call it out explicitly, and add it to the review checklist. - A handler that needed MouseEventArgs had to fetch the generic EventUtil overloads from the docs. Include Action and Func overloads in the snippet and show the explicit type argument usage. --- ...azor-rendering-performance.instructions.md | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index a55e13cbf..3dcd9d090 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -28,7 +28,8 @@ Rerendering, event, and virtualization guidance applies to components that rende ## Avoid Unnecessary Rerendering of Subtrees -- Give components that repeat at scale parameters of known immutable types. Pass the values the child needs (`OrderId="order.Id"`) rather than the whole model object when that keeps every parameter immutable. +- Give components that repeat at scale parameters of known immutable types. Pass the values the child needs (`OrderId="order.Id" Customer="@order.Customer"`) rather than the whole model object when that keeps every parameter immutable. +- Prefix expressions assigned to `string` parameters with `@`. Without it, Razor passes the attribute text literally: `Customer="order.Customer"` renders the text "order.Customer". - On repeated children, bind `EventCallback` parameters to method groups and let the child pass its own key back: ``, then `OnSelect.InvokeAsync(OrderId)` in the child. A lambda that captures a loop variable (`OnSelect="() => SelectOrder(order.Id)"`) produces a new delegate target on every render, so the child is always seen as changed. - When a child must accept complex parameters, override `ShouldRender` and compare a cheap change key captured in `OnParametersSet`: @@ -240,7 +241,12 @@ Rerendering, event, and virtualization guidance applies to components that rende public static Func AsNonRenderingEventHandler(Func handler) => new NonRenderingTarget(null, handler).InvokeAsync; - // Add Action and Func overloads the same way for handlers that take event args. + public static Action AsNonRenderingEventHandler(Action handler) + => new NonRenderingTarget(handler, null).Invoke; + + public static Func AsNonRenderingEventHandler(Func handler) + => new NonRenderingTarget(null, handler).InvokeAsync; + private sealed class NonRenderingTarget(Action? handler, Func? asyncHandler) : IHandleEvent { public void Invoke() => handler!(); @@ -248,10 +254,18 @@ Rerendering, event, and virtualization guidance applies to components that rende Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? arg) => item.InvokeAsync(arg); } + + private sealed class NonRenderingTarget(Action? handler, Func? asyncHandler) : IHandleEvent + { + public void Invoke(T arg) => handler!(arg); + public Task InvokeAsync(T arg) => asyncHandler!(arg); + + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? arg) => item.InvokeAsync(arg); + } } ``` - Use it as `@onclick="EventUtil.AsNonRenderingEventHandler(TrackClick)"`. + Use it as `@onclick="EventUtil.AsNonRenderingEventHandler(TrackClick)"`, or pass the event args type explicitly when the handler takes them: `@onclick="EventUtil.AsNonRenderingEventHandler(TrackClickAsync)"`. - Exceptions thrown by these handlers don't reach an `ErrorBoundary`. If you rely on error boundaries, catch the exception and call `await DispatchExceptionAsync(ex)`. ### Call `StateHasChanged` Only When the Framework Can't Render for You @@ -303,7 +317,7 @@ Rerendering, event, and virtualization guidance applies to components that rende ## Review Checklist for Components Rendered at Scale - [ ] Long scrollable collections in interactive components use `` with an accurate `ItemSize`. -- [ ] Repeated children receive only known immutable parameters, or override `ShouldRender` with a cheap change key that local state changes also update. +- [ ] Repeated children receive only known immutable parameters (with `@` before expressions assigned to `string` parameters), or override `ShouldRender` with a cheap change key that local state changes also update. - [ ] Items without independent state are inlined or rendered through a `RenderFragment` rather than one component each. - [ ] Event handlers in large loops don't use lambdas that capture the loop variable, and repeated children get method-group `EventCallback`s. - [ ] No `CaptureUnmatchedValues` on per-row or per-cell components. From d3859f5ca49828bf2f947bf20dc8b80c7f21021b Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 18:15:24 +0200 Subject: [PATCH 3/9] Address review: qualify DateOnly/TimeOnly and focusable scroll regions - DateOnly and TimeOnly only became known immutable types for change detection in .NET 10 (they're absent from ChangeDetection.cs in release/8.0 and release/9.0), so list the .NET 8 baseline and call out the .NET 10 addition. - Recommend tabindex="0" with an accessible name for standalone virtualized scroll regions so keyboard users can tab to them, and keep tabindex="-1" for containers that code focuses explicitly. --- instructions/blazor-rendering-performance.instructions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index 3dcd9d090..3f283a623 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -22,7 +22,7 @@ Rerendering, event, and virtualization guidance applies to components that rende ## Understand When Blazor Rerenders - After an event handler runs, `ComponentBase` rerenders the component that owns the handler. Each child then receives a new set of parameters and rerenders too, recursively, unless change detection proves nothing changed or its `ShouldRender` returns `false`. -- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. Currently these are primitives (`bool`, `char`, integral types, `float`, `double`), `decimal`, `string`, `DateTime`, enums, `Guid`, `DateOnly`, `TimeOnly`, `EventCallback`, and `EventCallback`. The framework can change this list between releases. +- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. On .NET 8 and later these are primitives (`bool`, `char`, integral types, `float`, `double`), `decimal`, `string`, `DateTime`, enums, `Guid`, `EventCallback`, and `EventCallback`. .NET 10 adds `DateOnly` and `TimeOnly`. The framework can change this list between releases. - Any other parameter type counts as "may have changed" on every parent render, even when the value is identical. This includes records, `DateTimeOffset`, `TimeSpan`, tuples, custom structs, collections, class instances, and `RenderFragment` (so any component that takes `ChildContent`). - `ShouldRender` is not consulted for the first render. A component always renders when it's first added to the tree. @@ -72,12 +72,12 @@ Rerendering, event, and virtualization guidance applies to components that rende - Inside a ``, set `SpacerElement="tr"` and render one `` per item. - Provide `` content when items load asynchronously and `` for empty results. - Call `RefreshDataAsync()` on the `Virtualize` reference when data behind an `ItemsProvider` changes. If that happens outside a Blazor event or lifecycle method, wrap the refresh and `StateHasChanged()` in `InvokeAsync`. -- Make the scroll container focusable (for example `tabindex="-1"`) so keyboard scrolling works in Chromium-based browsers. +- Make the scroll container focusable so keyboard scrolling works in Chromium-based browsers. Use `tabindex="0"` so keyboard users can tab to a standalone scroll region, and give it an accessible name (`role="region" aria-label="Orders"`). Reserve `tabindex="-1"` for containers that code focuses explicitly. ```razor @inject IOrderService Orders -
    +
    From 9fd822b5e731f1c576648b868c1be331ee15ea6c Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 18:24:42 +0200 Subject: [PATCH 4/9] Address review: exclude nint/nuint and include nullable value types Change detection excludes IntPtr/UIntPtr (nint/nuint), while nullable forms of the supported value types are skipped too: a non-null T? boxes as T and two null values compare as unchanged. Verified in a .NET 10 app: children with int?, int? null, and DateTime? parameters kept 1 render across 3 parent renders, while nint and TimeSpan children rerendered every time. --- instructions/blazor-rendering-performance.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index 3f283a623..996d494c8 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -22,7 +22,7 @@ Rerendering, event, and virtualization guidance applies to components that rende ## Understand When Blazor Rerenders - After an event handler runs, `ComponentBase` rerenders the component that owns the handler. Each child then receives a new set of parameters and rerenders too, recursively, unless change detection proves nothing changed or its `ShouldRender` returns `false`. -- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. On .NET 8 and later these are primitives (`bool`, `char`, integral types, `float`, `double`), `decimal`, `string`, `DateTime`, enums, `Guid`, `EventCallback`, and `EventCallback`. .NET 10 adds `DateOnly` and `TimeOnly`. The framework can change this list between releases. +- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. On .NET 8 and later these are `bool`, `char`, the integer types except `nint` and `nuint`, `float`, `double`, `decimal`, `string`, `DateTime`, enums, `Guid`, `EventCallback`, and `EventCallback`, plus the nullable forms of those value types (`int?`, `DateTime?`). .NET 10 adds `DateOnly` and `TimeOnly`. The framework can change this list between releases. - Any other parameter type counts as "may have changed" on every parent render, even when the value is identical. This includes records, `DateTimeOffset`, `TimeSpan`, tuples, custom structs, collections, class instances, and `RenderFragment` (so any component that takes `ChildContent`). - `ShouldRender` is not consulted for the first render. A component always renders when it's first added to the tree. From acf0c8b7ebf8d7603e12175e5835b6fa50601248 Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 18:35:06 +0200 Subject: [PATCH 5/9] Address review: list supported integer types explicitly Int128 and UInt128 report TypeCode.Object, so ChangeDetection treats them as possibly changed, like nint and nuint. List the supported integer widths explicitly instead of describing exclusions; the next bullet already covers every other type. --- instructions/blazor-rendering-performance.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index 996d494c8..b7988a71c 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -22,7 +22,7 @@ Rerendering, event, and virtualization guidance applies to components that rende ## Understand When Blazor Rerenders - After an event handler runs, `ComponentBase` rerenders the component that owns the handler. Each child then receives a new set of parameters and rerenders too, recursively, unless change detection proves nothing changed or its `ShouldRender` returns `false`. -- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. On .NET 8 and later these are `bool`, `char`, the integer types except `nint` and `nuint`, `float`, `double`, `decimal`, `string`, `DateTime`, enums, `Guid`, `EventCallback`, and `EventCallback`, plus the nullable forms of those value types (`int?`, `DateTime?`). .NET 10 adds `DateOnly` and `TimeOnly`. The framework can change this list between releases. +- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. On .NET 8 and later these are `bool`, `char`, `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `decimal`, `string`, `DateTime`, enums, `Guid`, `EventCallback`, and `EventCallback`, plus the nullable forms of those value types (`int?`, `DateTime?`). .NET 10 adds `DateOnly` and `TimeOnly`. The framework can change this list between releases. - Any other parameter type counts as "may have changed" on every parent render, even when the value is identical. This includes records, `DateTimeOffset`, `TimeSpan`, tuples, custom structs, collections, class instances, and `RenderFragment` (so any component that takes `ChildContent`). - `ShouldRender` is not consulted for the first render. A component always renders when it's first added to the tree. From ec7350dbd96b2a744056e5dd637ae543b0d23794 Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 18:48:05 +0200 Subject: [PATCH 6/9] Address review: qualify version-specific EventCallback and Virtualize guidance - On .NET 8 and 9, EventCallback.Equals compares delegate references (value equality arrived in .NET 10), and every instance method-group conversion allocates a new delegate. Recommend method groups only on .NET 10+, and a callback created once with EventCallback.Factory.Create on .NET 8 and 9. Verified: after 3 parent renders, a method-group child rendered 4 times on .NET 9.0.20 and once on .NET 10.0.12, while a cached callback rendered once on both. - .NET 11 positions virtualized items using a running average of measured heights, so the same-height requirement applies to .NET 8 through 10 and ItemSize becomes an initial estimate on .NET 11. --- .../blazor-rendering-performance.instructions.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index b7988a71c..f3df57d2c 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -30,7 +30,8 @@ Rerendering, event, and virtualization guidance applies to components that rende - Give components that repeat at scale parameters of known immutable types. Pass the values the child needs (`OrderId="order.Id" Customer="@order.Customer"`) rather than the whole model object when that keeps every parameter immutable. - Prefix expressions assigned to `string` parameters with `@`. Without it, Razor passes the attribute text literally: `Customer="order.Customer"` renders the text "order.Customer". -- On repeated children, bind `EventCallback` parameters to method groups and let the child pass its own key back: ``, then `OnSelect.InvokeAsync(OrderId)` in the child. A lambda that captures a loop variable (`OnSelect="() => SelectOrder(order.Id)"`) produces a new delegate target on every render, so the child is always seen as changed. +- On repeated children, let the child pass its own key back (`OnSelect.InvokeAsync(OrderId)`) instead of capturing the item in a lambda. A lambda that captures a loop variable (`OnSelect="() => SelectOrder(order.Id)"`) produces a new delegate target on every render, so the child is always seen as changed. +- Keep that `EventCallback` equal across renders. On .NET 10 and later, bind a method group: ``. On .NET 8 and 9, `EventCallback` equality compares delegate references and every method-group conversion creates a new delegate, so create the callback once and reuse it: assign `selectOrder = EventCallback.Factory.Create(this, SelectOrder);` in `OnInitialized`, then pass `OnSelect="selectOrder"`. - When a child must accept complex parameters, override `ShouldRender` and compare a cheap change key captured in `OnParametersSet`: ```razor @@ -68,7 +69,8 @@ Rerendering, event, and virtualization guidance applies to components that rende - `Virtualize` renders no items until its JavaScript side reports the viewport size, so it only shows items once the component is interactive: nothing during static SSR or prerendering. Page the data on the server for statically rendered lists. - Use `Items` for an in-memory `ICollection`. Use `ItemsProvider` for large or remote data sets, or non-generic sources such as `DataRow`, and never set both (the component throws `InvalidOperationException`). - In an items provider, fetch only `request.Count` items starting at `request.StartIndex`, pass `request.CancellationToken` to the data call, and return the total item count in `ItemsProviderResult`. -- Set `ItemSize` to the rendered item height in pixels (default `50`) so the first render and the scroll position are correct. Keep items and placeholder content the same height, rendered as a single vertical stack (`display: block` or `table-row`), and don't style the spacer elements. +- Set `ItemSize` to the rendered item height in pixels (default `50`) so the first render and the scroll position are correct. On .NET 8 through 10, keep items and placeholder content the same height. .NET 11 treats `ItemSize` as an initial estimate and positions items using a running average of measured heights. +- In every version, render items as a single vertical stack (`display: block` or `table-row`) and don't style the spacer elements. - Inside a ``, set `SpacerElement="tr"` and render one `` per item. - Provide `` content when items load asynchronously and `` for empty results. - Call `RefreshDataAsync()` on the `Virtualize` reference when data behind an `ItemsProvider` changes. If that happens outside a Blazor event or lifecycle method, wrap the refresh and `StateHasChanged()` in `InvokeAsync`. @@ -278,7 +280,7 @@ Rerendering, event, and virtualization guidance applies to components that rende ### Don't Recreate Delegates for Many Repeated Elements - A lambda that captures the loop variable (`@onclick="() => Select(item.Id)"`) creates a new delegate for every element on every render. Blazor then treats each handler as changed, replaces its event handler registration, and sends an attribute update per element (over the circuit for Interactive Server). -- That's fine for a handful of elements. For large lists, create the delegates once and reuse them, or move the item into a child component that takes an immutable key and a method-group `EventCallback`. +- That's fine for a handful of elements. For large lists, create the delegates once and reuse them, or move the item into a child component that takes an immutable key and a stable `EventCallback` (a method group on .NET 10 and later, a cached callback on .NET 8 and 9). ```razor @inject ICatalogService Catalog @@ -319,7 +321,7 @@ Rerendering, event, and virtualization guidance applies to components that rende - [ ] Long scrollable collections in interactive components use `` with an accurate `ItemSize`. - [ ] Repeated children receive only known immutable parameters (with `@` before expressions assigned to `string` parameters), or override `ShouldRender` with a cheap change key that local state changes also update. - [ ] Items without independent state are inlined or rendered through a `RenderFragment` rather than one component each. -- [ ] Event handlers in large loops don't use lambdas that capture the loop variable, and repeated children get method-group `EventCallback`s. +- [ ] Event handlers in large loops don't use lambdas that capture the loop variable, and repeated children get stable `EventCallback`s (method groups on .NET 10 and later, cached callbacks on .NET 8 and 9). - [ ] No `CaptureUnmatchedValues` on per-row or per-cell components. - [ ] `` uses `IsFixed="true"` for values that never change. - [ ] High-frequency DOM events are throttled in JavaScript. From fd3b9cb37e46e70fe1264e6d44ae2d64eb3fc051 Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 19:01:13 +0200 Subject: [PATCH 7/9] Address review: parameters that stay null don't force a rerender ChangeDetection.MayHaveChanged returns false when the old and new values are both null, before it looks at the runtime type. State that exception so Copilot doesn't add ShouldRender logic for optional complex parameters that stay null. Verified on .NET 10.0.12: after 3 parent renders, children with a null object or null RenderFragment parameter rendered once, while a child with a non-null object rendered 4 times. --- instructions/blazor-rendering-performance.instructions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index f3df57d2c..1647b7622 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -22,8 +22,8 @@ Rerendering, event, and virtualization guidance applies to components that rende ## Understand When Blazor Rerenders - After an event handler runs, `ComponentBase` rerenders the component that owns the handler. Each child then receives a new set of parameters and rerenders too, recursively, unless change detection proves nothing changed or its `ShouldRender` returns `false`. -- Change detection skips a child only when **every** parameter is a known immutable type whose value hasn't changed. On .NET 8 and later these are `bool`, `char`, `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `decimal`, `string`, `DateTime`, enums, `Guid`, `EventCallback`, and `EventCallback`, plus the nullable forms of those value types (`int?`, `DateTime?`). .NET 10 adds `DateOnly` and `TimeOnly`. The framework can change this list between releases. -- Any other parameter type counts as "may have changed" on every parent render, even when the value is identical. This includes records, `DateTimeOffset`, `TimeSpan`, tuples, custom structs, collections, class instances, and `RenderFragment` (so any component that takes `ChildContent`). +- Change detection skips a child only when **every** parameter is either `null` in both renders or a known immutable type whose value hasn't changed. On .NET 8 and later the known immutable types are `bool`, `char`, `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `decimal`, `string`, `DateTime`, enums, `Guid`, `EventCallback`, and `EventCallback`, plus the nullable forms of those value types (`int?`, `DateTime?`). .NET 10 adds `DateOnly` and `TimeOnly`. The framework can change this list between releases. +- A non-null value of any other type counts as "may have changed" on every parent render, even when the value is identical. This includes records, `DateTimeOffset`, `TimeSpan`, tuples, custom structs, collections, class instances, and `RenderFragment` (so any component that receives child content). - `ShouldRender` is not consulted for the first render. A component always renders when it's first added to the tree. ## Avoid Unnecessary Rerendering of Subtrees From 4c722da2ccdfc539ba8d82e3fd9c1193b4251ac3 Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 19:35:19 +0200 Subject: [PATCH 8/9] Address review: equal-height Virtualize items and intermediate async renders - The virtualization docs still require identical item and placeholder heights on .NET 11, so keep that rule for every version and describe the .NET 11 running average of measured heights separately. - ComponentBase renders when an async event handler or lifecycle method first yields and when it completes, but not between later awaits (see RunInitAndSetParametersAsync). Limit the "don't call StateHasChanged" advice to the end of handlers and lifecycle methods, and cover intermediate progress in multi-step async lifecycle methods too. --- instructions/blazor-rendering-performance.instructions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index 1647b7622..f871dce32 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -69,8 +69,8 @@ Rerendering, event, and virtualization guidance applies to components that rende - `Virtualize` renders no items until its JavaScript side reports the viewport size, so it only shows items once the component is interactive: nothing during static SSR or prerendering. Page the data on the server for statically rendered lists. - Use `Items` for an in-memory `ICollection`. Use `ItemsProvider` for large or remote data sets, or non-generic sources such as `DataRow`, and never set both (the component throws `InvalidOperationException`). - In an items provider, fetch only `request.Count` items starting at `request.StartIndex`, pass `request.CancellationToken` to the data call, and return the total item count in `ItemsProviderResult`. -- Set `ItemSize` to the rendered item height in pixels (default `50`) so the first render and the scroll position are correct. On .NET 8 through 10, keep items and placeholder content the same height. .NET 11 treats `ItemSize` as an initial estimate and positions items using a running average of measured heights. -- In every version, render items as a single vertical stack (`display: block` or `table-row`) and don't style the spacer elements. +- Set `ItemSize` to the rendered item height in pixels (default `50`) so the first render and the scroll position are correct. On .NET 11, `ItemSize` is only the initial estimate: the component then positions items using a running average of measured heights. +- In every version, keep items and placeholder content the same height, render them as a single vertical stack (`display: block` or `table-row`), and don't style the spacer elements. - Inside a ``, set `SpacerElement="tr"` and render one `` per item. - Provide `` content when items load asynchronously and `` for empty results. - Call `RefreshDataAsync()` on the `Virtualize` reference when data behind an `ItemsProvider` changes. If that happens outside a Blazor event or lifecycle method, wrap the refresh and `StateHasChanged()` in `InvokeAsync`. @@ -272,8 +272,8 @@ Rerendering, event, and virtualization guidance applies to components that rende ### Call `StateHasChanged` Only When the Framework Can't Render for You -- Don't call it at the end of event handlers or in `OnInitialized{Async}` and `OnParametersSet{Async}`. `ComponentBase` already renders at those points. -- Do call it to show intermediate progress in a multi-step async handler, to refresh a component from outside Blazor's event system (timers, C# events raised by a state container, background work), wrapped in `InvokeAsync(...)` when running off the renderer's synchronization context, or to rerender a component outside the subtree that handled the event. +- Don't call it at the end of an event handler or lifecycle method (`OnInitialized{Async}`, `OnParametersSet{Async}`). `ComponentBase` already renders there, and when an async handler or lifecycle method first yields at an `await`. +- Do call it to show intermediate progress between later `await`s of a multi-step async event handler or lifecycle method, to refresh a component from outside Blazor's event system (timers, C# events raised by a state container, background work), wrapped in `InvokeAsync(...)` when running off the renderer's synchronization context, or to rerender a component outside the subtree that handled the event. - To render partway through otherwise synchronous work, call `StateHasChanged()` followed by `await Task.Yield()`, not `await Task.Delay(1)`. - Never call `StateHasChanged()` unconditionally from `OnAfterRender{Async}`, which creates a render loop. Guard it with `firstRender` or an explicit state check. From de272440dba9fe7b920c11836fe1eb6163d61511 Mon Sep 17 00:00:00 2001 From: Adrien Clerbois Date: Wed, 23 Sep 2026 19:45:56 +0200 Subject: [PATCH 9/9] Address review: .NET 11 variable-height Virtualize items, camelCase fragment field - The "What's new in ASP.NET Core in .NET 11" release notes state that Virtualize no longer assumes every item has the same height and adapts to item sizes measured at runtime (dotnet/aspnetcore#66521). Limit the same-height rule to .NET 8 through 10 and keep the single-vertical-stack and spacer rules for every version. - Rename the private RenderFragment field in the example to camelCase (logLine) to match the other private fields in the guidance. --- instructions/blazor-rendering-performance.instructions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/instructions/blazor-rendering-performance.instructions.md b/instructions/blazor-rendering-performance.instructions.md index f871dce32..a869f5a82 100644 --- a/instructions/blazor-rendering-performance.instructions.md +++ b/instructions/blazor-rendering-performance.instructions.md @@ -69,8 +69,8 @@ Rerendering, event, and virtualization guidance applies to components that rende - `Virtualize` renders no items until its JavaScript side reports the viewport size, so it only shows items once the component is interactive: nothing during static SSR or prerendering. Page the data on the server for statically rendered lists. - Use `Items` for an in-memory `ICollection`. Use `ItemsProvider` for large or remote data sets, or non-generic sources such as `DataRow`, and never set both (the component throws `InvalidOperationException`). - In an items provider, fetch only `request.Count` items starting at `request.StartIndex`, pass `request.CancellationToken` to the data call, and return the total item count in `ItemsProviderResult`. -- Set `ItemSize` to the rendered item height in pixels (default `50`) so the first render and the scroll position are correct. On .NET 11, `ItemSize` is only the initial estimate: the component then positions items using a running average of measured heights. -- In every version, keep items and placeholder content the same height, render them as a single vertical stack (`display: block` or `table-row`), and don't style the spacer elements. +- Set `ItemSize` to the rendered item height in pixels (default `50`) so the first render and the scroll position are correct. On .NET 11, `ItemSize` is only the initial estimate: the component measures items at runtime, positions them using a running average of measured heights, and no longer assumes that every item has the same height. +- On .NET 8 through 10, keep items and placeholder content the same height. In every version, render items and placeholders as a single vertical stack (`display: block` or `table-row`), and don't style the spacer elements. - Inside a ``, set `SpacerElement="tr"` and render one `` per item. - Provide `` content when items load asynchronously and `` for empty results. - Call `RefreshDataAsync()` on the `Virtualize` reference when data behind an `ItemsProvider` changes. If that happens outside a Blazor event or lifecycle method, wrap the refresh and `StateHasChanged()` in `InvokeAsync`. @@ -115,12 +115,12 @@ Rerendering, event, and virtualization guidance applies to components that rende
      @foreach (var entry in logEntries) { - @LogLine(entry) + @logLine(entry) }
    @code { - private RenderFragment LogLine = entry => + private RenderFragment logLine = entry => @
  • @entry.Timestamp.ToString("T") @entry.Message
  • ; } ```