Skip to content

Add Blazor rendering performance instructions - #3718

Open
AClerbois wants to merge 9 commits into
github:mainfrom
AClerbois:feature/blazor-rendering-performance
Open

AClerbois wants to merge 9 commits into
github:mainfrom
AClerbois:feature/blazor-rendering-performance

Conversation

@AClerbois

@AClerbois AClerbois commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Checklist

  • I have read and followed the CONTRIBUTING.md guidelines.
  • I have read and followed the Guidance for submissions involving paid services.
  • My contribution adds a new instruction, prompt, agent, skill, workflow, or canvas extension file in the correct directory.
  • The file follows the required naming convention.
  • The content is clearly structured and follows the example format.
  • I have tested my instructions, prompt, agent, skill, workflow, or canvas extension with GitHub Copilot.
  • I have run npm start and verified that README.md is up to date.
  • I am targeting the main branch for this pull request.

Description

Adds instructions/blazor-rendering-performance.instructions.md (applyTo: '**/*.razor, **/*.razor.cs'). It turns the official ASP.NET Core Blazor rendering performance best practices into rules Copilot can apply while writing or reviewing Razor components:

  • When to optimize: hot paths only (components repeated at scale, high-frequency rerenders), so Copilot doesn't add ShouldRender, SetParametersAsync, or IHandleEvent to every component.
  • How change detection works: the exact list of known immutable parameter types from ChangeDetection.cs. Records, DateTimeOffset, TimeSpan, collections, and RenderFragment/ChildContent always trigger a child rerender.
  • Skipping subtree rerenders: immutable parameters, method-group EventCallbacks instead of capturing lambdas, and a ShouldRender change-key pattern that includes the stale-UI pitfall when the component also changes local state.
  • Virtualization: Items vs. ItemsProvider, ItemSize, SpacerElement, placeholders, keyboard focus, and the fact that Virtualize renders no items during static SSR or prerendering.
  • Lightweight components at scale: inlining vs. child components, RenderFragment templates, parameter count, IsFixed cascading values, attribute splatting, manual SetParametersAsync as a last resort (small gain on .NET 10+), and literal sequence numbers in RenderTreeBuilder code.
  • Events: throttling high-frequency DOM events through JS interop, non-rendering handlers (IHandleEvent, including the ErrorBoundary caveat), when StateHasChanged is and isn't needed, pre-created delegates in large loops, and @key.
  • A review checklist for components rendered at scale.

How it differs from existing resources

blazor.instructions.md covers general Blazor conventions, and its performance guidance is five generic bullets (for example "use ShouldRender() where appropriate"). This file is dedicated to rendering performance. It explains the mechanics that decide whether a rerender happens, the trade-offs between techniques, and when not to optimize.

Validation

  • Checked every technical claim against the Microsoft Learn articles (rendering performance, component rendering, virtualization, @key) and the ASP.NET Core reference source (ChangeDetection.cs, EventCallback.cs, EventCallbackFactory.cs, RenderTreeDiffBuilder.cs, Virtualize.cs).
  • Extracted every code sample verbatim from the instruction file into a .NET 10 Blazor Web App (Interactive Server). The build succeeded with 0 warnings and 0 errors.
  • Ran these checks in the browser against that app:
    • After a parent rerender, children bound to a method-group EventCallback stayed at 1 render. Children bound to a capturing lambda went to 2.
    • The ShouldRender sample didn't rerender for an unchanged order. It rerendered when the order version changed and when its own event handler ran.
    • IHandleEvent and the EventUtil non-rendering handlers (sync, async, and with MouseEventArgs) didn't trigger a render. An explicit StateHasChanged() still did.
    • Virtualize with an ItemsProvider over 100,000 items kept 21 rows in the DOM.

A/B evaluation in simulated Copilot agent-mode sessions

Each scenario ran with the same model (Claude Sonnet, also selectable in GitHub Copilot) and the same prompt. The only difference was whether the instruction file was attached, the way VS Code attaches instructions whose applyTo matches the edited files. I built the generated code (Blazor Server, .NET 10) and measured it in the browser.

Scenario Without the instruction With the instruction
Table of 5,000 orders: payload received per row click 1,714 bytes (every visible row re-diffed, per-row lambdas re-created) 622 bytes (unchanged OrderRow components skipped)
Virtualize spacers inside <tbody> <div> (invalid table markup) <tr> (SpacerElement="tr")
Scrolling to 50% of the list Lands on order #2653 (ItemSize="40" with 38 px rows) Lands on order #2501 (ItemSize matches the rendered row height)
Keyboard-scrollable container No Yes (tabindex="-1")
Pointer tracking, 100 moves in about 1 s: messages sent / bytes received 200 / 24.3 KB (@onpointermove bound directly) 40 / 5.7 KB (throttled in JavaScript)
Analytics click that changes nothing on screen Triggers a render No render (non-rendering handler); the click is still tracked

The evaluation also improved the instruction itself:

  • The first instructed run passed string parameters without @ (Customer="order.Customer"), so the literal text was rendered. The instruction now calls this out, and a second run rendered correctly.
  • The instructed pointer session had to look up the event-args overloads of the non-rendering handler helper, so those overloads are now part of the snippet.

Type of Contribution

  • New instruction file.
  • New prompt file.
  • New agent file.
  • New plugin.
  • New skill file.
  • New agentic workflow.
  • New canvas extension.
  • Update to existing instruction, prompt, agent, plugin, skill, workflow, or canvas extension.
  • Other (please specify):

Additional Notes

Source article: https://learn.microsoft.com/aspnet/core/blazor/performance/rendering. The guidance is identical in the .NET 10 and .NET 11 versions of that article.


By submitting this pull request, I confirm that my contribution abides by the Code of Conduct and will be licensed under the MIT License.

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.
…tion

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<T> and Func<T, Task> overloads
  in the snippet and show the explicit type argument usage.
Copilot AI balanced review requested due to automatic review settings September 23, 2026 16:06
@github-actions github-actions Bot added instructions PR touches instructions new-submission PR adds at least one new contribution labels Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔒 PR Risk Scan Results

Scanned 1 changed file(s).

Severity Count
🔴 High 0
🟠 Medium 0
ℹ️ Info 0

✅ No matching risk patterns were detected in changed files.

This is an automated soft-gate report. Findings indicate review targets and do not block merge by themselves.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The guidance misstates .NET 8 change detection and makes its sample scroll container unreachable through sequential keyboard navigation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 Low severity

Open (2)
What changed in this PR

Adds focused Blazor rendering-performance guidance for Razor components.

Changes:

  • Documents rerendering, virtualization, component, and event optimizations.
  • Adds practical examples and a review checklist.
  • Registers the instruction in the generated documentation index.
File Description
instructions/​blazor-rendering-performance.instructions.md Adds Blazor performance instructions and examples.
docs/​README.instructions.md Adds the instruction to the catalog.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread instructions/blazor-rendering-performance.instructions.md Outdated
Comment thread instructions/blazor-rendering-performance.instructions.md Outdated
- 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.
Copilot AI review requested due to automatic review settings September 23, 2026 16:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The immutable-type guidance misclassifies native integers and omits nullable supported value types.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (2)

Comment thread instructions/blazor-rendering-performance.instructions.md Outdated
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.
Copilot AI review requested due to automatic review settings September 23, 2026 16:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The immutable-type guidance incorrectly includes Int128 and UInt128 by implication.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

Comment thread instructions/blazor-rendering-performance.instructions.md Outdated
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.
Copilot AI review requested due to automatic review settings September 23, 2026 16:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Several recommendations incorrectly present version-specific .NET behavior as applying to all supported releases.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Low severity Qualify EventCallback optimization for .NET 8/9

instructions/​blazor-rendering-performance.instructions.md:33

This optimization only works as written on .NET 10+. In .NET 8 and 9, EventCallback.Equals compares the delegate by reference, while each instance method-group conversion creates a new delegate, so the child is still seen as changed on every parent render. Qualify the guidance or tell .NET 8/9 users to cache and reuse one EventCallback value.

This issue also appears in the following locations of the same file:

  • line 281
  • line 322
Low severity Qualify Virtualize fixed-height guidance for .NET 8–10

instructions/​blazor-rendering-performance.instructions.md:71

This is no longer release-independent guidance. In .NET 11, Virtualize measures actual item heights and maintains a running average, so ItemSize is an initial estimate and items no longer have to share one exact height. Since the file targets “.NET 8 or later,” qualify the fixed-height requirement as .NET 8–10 behavior.

… 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.
Copilot AI review requested due to automatic review settings September 23, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The change-detection guidance incorrectly omits the unchanged-null parameter case.

Review effort: Balanced
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Low severity Null complex parameters incorrectly skip child rerender checks

instructions/​blazor-rendering-performance.instructions.md:26

ChangeDetection.MayHaveChanged returns false before inspecting the runtime type when both the old and new values are null. Therefore an optional complex parameter (including ChildContent) that remains null does not force a child rerender, contrary to “only when every parameter is a known immutable type” and “every parent render.” Qualify this null case so Copilot doesn't add unnecessary ShouldRender logic.

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.
Copilot AI review requested due to automatic review settings September 23, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The guidance contains inaccurate virtualization and lifecycle-rendering rules that could produce stale or incorrectly virtualized UI.

Review effort: Balanced
Findings: None

Previously missed (2)

In code that hasn't changed since last review

Low severity Keep Virtualize equal-height requirement for all supported .NET versions

instructions/​blazor-rendering-performance.instructions.md:72

The running-average change in .NET 11 doesn't remove Virtualize's equal-height layout requirement. The current virtualization documentation still states that all rendered items, including placeholders, must have identical heights and that varying heights prevents correct operation. Limiting this rule to .NET 8–10 can make Copilot generate broken .NET 11 lists; keep the invariant for every supported version and describe the running average separately.

Low severity Clarify lifecycle guidance for intermediate async state updates

instructions/​blazor-rendering-performance.instructions.md:276

ComponentBase renders when an async lifecycle task is first returned and when it completes, but it can't detect intermediate awaited state changes. This blanket prohibition can leave progress from a multi-step OnInitializedAsync or OnParametersSetAsync stale. Limit the no-call advice to ordinary/end-of-method use and include lifecycle methods in the intermediate-progress exception.

…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.
Copilot AI review requested due to automatic review settings September 23, 2026 17:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The guidance incorrectly requires equal-height virtualized items on .NET 11 and includes one conflicting private-field naming example.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Low severity

Open (1)

Comment thread instructions/blazor-rendering-performance.instructions.md Outdated
…ragment 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.
Copilot AI review requested due to automatic review settings September 23, 2026 17:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The guidance is technically sound, differentiated, well validated, and follows repository requirements.

Review effort: Balanced
Findings: None

Resolved since last review (1)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

instructions PR touches instructions new-submission PR adds at least one new contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants