Skip to content

Allocate a component's second RenderTreeBuilder on demand - #70153

Open
davidfowl wants to merge 1 commit into
mainfrom
perf/components-lazy-next-render-tree
Open

davidfowl wants to merge 1 commit into
mainfrom
perf/components-lazy-next-render-tree

Conversation

@davidfowl

Copy link
Copy Markdown
Member

ComponentState eagerly creates two RenderTreeBuilders, each with its own RenderTreeFrameArrayBuilder and Stack<int>, so it can diff new output against the previous output. Many components only ever render once, and every component does during static SSR, so the second builder is never used. It stays allocated and retained for the component's lifetime, which in a Blazor Server circuit can be long.

Description

  • _nextRenderTree is now created only when a component re-renders.
  • A component's first render is written directly into CurrentRenderTree (which is empty at that point) and diffed against an empty ArrayRange.
  • If the first render throws, either from the render fragment or from AssertTreeIsValid, CurrentRenderTree is cleared. It keeps representing the previous (empty) output, as before.
  • From the second render onward, behaviour is the same as today.

Benchmarks

This adds the same HtmlRendererBenchmark as #70151, with identical files. RenderAndWriteHtml creates an HtmlRenderer, renders 101 components (a 100-row table whose rows are child components), and writes the HTML.

Local BenchmarkDotNet, InProcess, 3 interleaved rounds against main:

main PR
RenderAndWriteHtml allocated 141 KB 129 KB (−8.5%)
RenderAndWriteHtml time 110.9 / 106.0 / 105.1 µs 102.5 / 114.0 / 106.5 µs (neutral, within noise)
Retained memory per rendered component (GC.GetTotalMemory(true) with 50 live renderers) −127 B per component

Tests

  • Microsoft.AspNetCore.Components.Tests: 1305 passed, 8 skipped.
  • New tests:
    • ExceptionDuringFirstRender_LeavesRenderTreeEmpty_AndLaterRenderSucceeds
    • InvalidRenderTreeDuringFirstRender_LeavesRenderTreeEmpty
  • Microsoft.AspNetCore.Components.Web.Tests: 330 passed.
  • Microsoft.AspNetCore.Components.Endpoints.Tests: 883 passed.
  • Microsoft.AspNetCore.Components.Server.Tests: 394 passed.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

ComponentState eagerly created two RenderTreeBuilders (each with a
RenderTreeFrameArrayBuilder and a Stack<int>) so that it could diff the new
output against the previous one. Many components only ever render once, for
example every component during static SSR, so the second builder was never
used.

Write the first render directly into CurrentRenderTree, diffing it against an
empty tree, and only create the second builder when the component re-renders.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl
davidfowl requested a review from a team as a code owner October 31, 2026 00:44
Copilot AI balanced review requested due to automatic review settings October 31, 2026 00:44
@github-actions github-actions Bot added the area-blazor Includes: Blazor, Razor Components label Oct 31, 2026

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.

🟢 Approval recommended

No active review issues remain for the change that defers allocation of the second render-tree builder.

No open issues 🎉

What changed in this PR

This PR reduces per-component memory use in Blazor by allocating the second RenderTreeBuilder only when a component renders again, avoiding that allocation for components that render once.

Changes:

  • Writes the first render directly into the current tree and diffs it against an empty tree, preserving subsequent-render behavior.
  • Adds regression tests for first-render failures, empty-tree cleanup, and successful recovery.
  • Adds HTML-rendering benchmarks to measure rendering allocations and HTML-writing performance.
File Description
src/​Components/​Components/​test/​RendererTest.cs Tests first-render failure cleanup and later render recovery.
src/​Components/​Components/​src/​Rendering/​ComponentState.cs Defers the second builder allocation and handles first-render cleanup and nullable disposal.
src/​Components/​Components/​perf/​Microsoft.AspNetCore.Components.Performance.csproj References Components.Web for HTML-rendering benchmarks.
src/​Components/​Components/​perf/​HtmlRendererBenchmark.cs Adds full-render and HTML-writing benchmarks using a component-based table.

Review effort: Balanced

Copy link
Copy Markdown
Member Author

Compliance check: no public API changes. Rendered output, batches and diffs are identical, and first-render exceptions still leave CurrentRenderTree empty (two new tests cover this). For full transparency, there is one theoretical difference. If a component reentrantly reads its own frames (Renderer.GetCurrentRenderTreeFrames(ownId)) from inside its first BuildRenderTree call, it now sees partially built frames instead of an empty range. Nothing in the repo does this; every reader runs after the fragment completes, at which point the tree is the same as before. If maintainers consider that unacceptable, I can close the gap or drop the PR.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

davidfowl commented Oct 31, 2026 •

Copy link
Copy Markdown
Member Author

/azp run aspnetcore-components-e2e

Rerunning e2e. Both failures are known flaky tests unrelated to this change, per Build Analysis: ServerRoutingTest.NavigationLock_CanBlockExternalNavigation (25.42% historical failure rate) and VirtualizationTest.VariableHeight_HtmlTable_DoesNotProduceNestedTrElements (8.59%). The same tests also failed on unrelated PRs #70125 and #70139.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

davidfowl commented Oct 31, 2026 •

Copy link
Copy Markdown
Member Author

/azp run aspnetcore-components-e2e

Rerunning e2e. Only the Mono-server leg failed; CoreCLR and Mono/WebAssembly passed with this change. Failing tests, per Build Analysis:

If the QuickGrid test fails again, I'll investigate it as potentially related.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@PureWeen

PureWeen commented Nov 1, 2026

Copy link
Copy Markdown
Member

/review

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

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants