Hot reload: scope ShouldRender bypass to first render per component to prevent unbounded re-render loop (OOM)#67372
Merged
Merged
Conversation
…load Fixes the OOM infinite re-render loop (issue #67371). During a hot reload metadata-update pass, ComponentBase.StateHasChanged previously bypassed ShouldRender() for the entire pass. For apps that rely on ShouldRender() == false to break a synchronous re-render cycle this removed the brake, causing an unbounded loop. The fix: - Renderer tracks which component IDs have already received a hot-reload render in the current pass via a lazily-allocated HashSet<int> (_hotReloadRenderedComponentIds), which is nulled out when the pass ends so there is no allocation in the non-hot-reload path. - RenderHandle exposes IsFirstHotReloadRender() delegating to the renderer's new IsFirstHotReloadRender(componentId) method. - ComponentBase.StateHasChanged gates the ShouldRender bypass on IsRenderingOnMetadataUpdate && IsFirstHotReloadRender(), so the bypass applies exactly once per component. Subsequent StateHasChanged calls in the same pass go through ShouldRender() normally, terminating any synchronous re-render cycle. Regression test added: HotReload_ShouldRenderBypassScopedToFirstRender _TerminatesCyclicRenderLoop verifies two root components that mutually trigger each other's StateHasChanged in BuildRenderTree each render exactly once during a simulated hot reload (not 10+ times until OOM). Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Fix scope ShouldRender bypass to prevent unbounded re-render loop
Hot reload: scope ShouldRender bypass to first render per component to prevent unbounded re-render loop (OOM)
Jun 22, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a hot-reload regression in Blazor rendering where ComponentBase.StateHasChanged could bypass ShouldRender() for an entire metadata-update pass, enabling synchronous cyclic re-render loops that can lead to OOM. The fix scopes the ShouldRender bypass to the first hot-reload render per component, ensuring each component refreshes at least once while still allowing ShouldRender() == false to stop cycles on subsequent renders within the same pass.
Changes:
- Track per-pass “first hot reload render” per component via a lazily allocated
HashSet<int>inRenderer, cleared after the metadata-update pass. - Expose a renderer-backed
RenderHandle.IsFirstHotReloadRender()helper and tightenComponentBase.StateHasChangedto bypassShouldRender()only on a component’s first hot-reload render. - Add a regression test that reproduces the cyclic re-render loop scenario and asserts termination.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Components/Components/src/RenderTree/Renderer.cs | Adds per-hot-reload-pass component tracking to scope the ShouldRender bypass. |
| src/Components/Components/src/RenderHandle.cs | Adds IsFirstHotReloadRender() hook used by ComponentBase during hot reload. |
| src/Components/Components/src/ComponentBase.cs | Updates StateHasChanged hot-reload bypass condition to require “first render in pass”. |
| src/Components/Components/test/RendererTest.cs | Adds regression test covering a cyclic StateHasChanged loop during hot reload. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
pavelsavara
approved these changes
Jun 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
During a hot-reload metadata-update pass,
ComponentBase.StateHasChangedbypassedShouldRender()for the entire pass. Apps that rely onShouldRender() == falseto break a synchronous re-render cycle (component A renders → triggers B → B renders → triggers A → …) had that brake removed, causing an unbounded render loop that OOMs the process.Description
Scopes the
ShouldRenderbypass to each component's first render in the hot-reload pass. On any subsequentStateHasChangedcall within the same pass,ShouldRender()is evaluated normally, terminating the cycle. Every component still re-renders at least once (no stale UI) because the diff already force-callsSetDirectParameterstop-down on every component regardless.Motivation and Context
Apps hitting #67012 experienced memory growth to 6 GB+ and OS-level process kills after a hot reload. Root cause:
IsRenderingOnMetadataUpdatewas latchedtruefor the entire synchronous pass, so everyStateHasChangedcall inside that pass bypassedShouldRender, removing the only cycle-breaker in apps that relied on it.Changes Made
Renderer.cs— addsHashSet<int>? _hotReloadRenderedComponentIds(lazily allocated, nulled out after the pass — zero cost on the normal render path). New internal methodIsFirstHotReloadRender(int componentId)usesHashSet.Addto returntruethe first time a component is seen in the pass.RenderHandle.cs— addsIsFirstHotReloadRender()delegating to the renderer.ComponentBase.cs— tightens the bypass condition:&&short-circuit ensures theHashSetis only touched during a hot-reload pass.RendererTest.cs— regression testHotReload_ShouldRenderBypassScopedToFirstRender_TerminatesCyclicRenderLoop: two root components that mutually callStateHasChangedon each other fromBuildRenderTree, both withShouldRender() => false. Self-limits at 10 renders so CI fails fast without the fix rather than hanging/OOMing. Asserts each component renders exactly twice (once initial, once hot-reload forced).How Has This Been Tested?
New regression test covers the cycle scenario directly. All 1,267 existing
Microsoft.AspNetCore.Components.Teststests continue to pass.Types of changes
Checklist