Visitar URL original
perf: generate rendered component markup on demand by swisscoder · Pull Request #1979 · bUnit-dev/bUnit · GitHub
Skip to content

perf: generate rendered component markup on demand - #1979

Open
swisscoder wants to merge 1 commit into
bUnit-dev:mainfrom
swisscoder:perf/lazy-markup
Open

swisscoder wants to merge 1 commit into
bUnit-dev:mainfrom
swisscoder:perf/lazy-markup

Conversation

@swisscoder

@swisscoder swisscoder commented Nov 12, 2026 •

Copy link
Copy Markdown

Pull request description

A component returned to a test (the cut, or one found via FindComponent/FindComponents) is notified through UpdateParents once per changed child in a render batch. On every notification RenderedComponent.UpdateState regenerated the full markup of its subtree with Htmlizer.GetHtml, so the same markup was rebuilt many times per batch, while the test reads it at most once.

This PR generates the markup on demand: UpdateState only marks it as stale, and Markup (and with it Nodes and Find) generates it on the next read.

Why: in a real-world test suite (Blazor pages with ~100 KB of rendered markup), a single state change regenerated the page markup 44 to 284 times; a CPU profile attributed ~40 % of the test time to UpdateMarkup. Measured with this patch on two affected test projects (same machine, alternating runs, all tests green):

Test project before after
Project A 170 s / 115 s 87 s / 74 s
Project B 173 s / 189 s 77 s / 82 s

Semantics kept:

  • OnMarkupUpdated is still raised after each render that changes the markup, so the WaitFor* helpers and other subscribers behave as before: they read Markup, which generates it once.
  • Generation runs under the renderer's render tree lock (new internal BunitRenderer.ReadRenderTree, the same lock FindComponents uses), so no render batch can change the tree while it is read. The lock is re-entrant for handlers invoked from UpdateDisplayAsync. After the renderer is disposed, the last generated markup is returned.
  • No public API change, no breaking change.

Tests: four new tests in RenderedComponentTest (repeated reads return the same instance; markup reflects the last of several unread renders; OnMarkupUpdated subscribers read the updated markup; a parent's markup reflects a render of a child found via FindComponent). Full suite green locally on net8.0, net9.0, net10.0 and net11.0.

PR meta checklist

  • Pull request is targeted at main branch for code
    or targeted at stable branch for documentation that is live on bunit.dev.
  • Pull request is linked to all related issues, if any.
  • I have read the CONTRIBUTING.md document.

Code PR specific checklist

  • My code follows the code style of this project and AspNetCore coding guidelines.
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly.
  • I have updated the appropriate sub section in the CHANGELOG.md.
  • I have added, updated or removed tests to according to my changes.
    • All tests passed.

A component returned to a test (the cut, or one found via FindComponent
or FindComponents) is notified through UpdateParents once per changed
child in a render batch. Each notification regenerated the full markup of
its subtree with Htmlizer.GetHtml, so the same markup was rebuilt many
times per batch while the test read it at most once.

Measured in a real-world suite: one state change on a page with ~100 KB of
markup regenerated the page markup 44 to 284 times; a CPU profile of the
test attributed ~40 % of its time to UpdateMarkup.

UpdateState now only marks the markup as stale and still raises
OnMarkupUpdated, so the WaitFor helpers and other subscribers keep their
semantics: they read Markup, which generates it once. Generation runs
under the renderer's render tree lock (ReadRenderTree), like
FindComponents, so no batch can change the tree while it is read; the lock
is re-entrant for handlers invoked from UpdateDisplayAsync.
@swisscoder

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant