ASP.NET Core Blazor Components Rendering and State Management: A Deep Dive into the Source Code
Blazor components rely on a diff-based render tree architecture where ComponentBase orchestrates lifecycle events, RenderTreeBuilder constructs UI frames, and StateHasChanged() triggers efficient DOM updates through the RenderTreeDiffBuilder algorithm.
Blazor abstracts browser rendering into C# through a sophisticated pipeline that operates identically across Server and WebAssembly hosting models. Understanding how ASP.NET Core Blazor Components rendering and state management works requires examining the framework's core implementation in the dotnet/aspnetcore repository. This article explores the actual source code for the render tree construction, diffing algorithm, and state notification system that powers interactive Blazor applications.
The Component Foundation and Render Tree Structure
Blazor components are built on a thin abstraction layer centered around ComponentBase, which implements the core rendering contracts IComponent, IHandleEvent, and IHandleAfterRender. The rendering engine works with a render tree composed of RenderTreeFrame structs that describe the UI structure declaratively.
When a component renders, its BuildRenderTree method—either generated from Razor markup or overridden manually—populates a RenderFragment delegate. This delegate receives a RenderTreeBuilder instance that creates the actual frame sequence. As implemented in src/Components/Components/src/Rendering/RenderTreeBuilder.cs, the builder emits frames representing elements, text nodes, component references, and attributes.
The Seven-Step Rendering Pipeline
The Blazor rendering pipeline follows a precise sequence of operations that transform component state into UI updates:
- Component instantiation – The framework creates an instance of the component class, typically derived from
ComponentBase. - Initialization –
OnInitializedandOnInitializedAsyncexecute once after the component receives its initial parameters. - Parameter assignment – When parent components supply new parameter values,
OnParametersSetandOnParametersSetAsyncrun to allow state recalculation. - Render tree construction – The component's
BuildRenderTreemethod invokesRenderTreeBuildermethods to emit the frame sequence. - Diff calculation – The new render tree is compared against the previous version by
RenderTreeDiffBuilder, located insrc/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs. Only the minimal set of changes is computed. - Commit phase – Changes are applied to the actual DOM in WebAssembly mode, or serialized and sent via SignalR to a
RemoteRendererin Server-Side Blazor. - Post-render callbacks –
OnAfterRenderandOnAfterRenderAsyncfire after the UI updates complete, providing access tofirstRenderboolean to distinguish initial renders from updates.
State Management and the StateHasChanged Mechanism
Component UI refreshes are triggered by calling StateHasChanged(), implemented in src/Components/Components/src/ComponentBase.cs. This method validates three conditions before queuing a render operation:
- No render is currently pending (checked via
_hasPendingQueuedRender). - The component has never rendered,
ShouldRender()returnstrue, or a hot-reload metadata update is occurring (_renderHandle.IsRenderingOnMetadataUpdate).
When conditions are satisfied, the stored _renderFragment is passed to the RenderHandle, which initiates the diff-and-commit cycle. Developers can customize rendering decisions by overriding the protected virtual bool ShouldRender() method to return false when state mutations should not trigger visual updates.
Server-Side vs. WebAssembly Rendering Models
Despite identical component code, the hosting model determines how render tree diffs reach the browser:
- Server-Side Blazor – Components execute on the server within
RemoteRenderer(src/Components/Server/src/RemoteRenderer.cs). The diff is serialized and transmitted over a SignalR connection to a thin JavaScript client that applies changes to the browser DOM. - WebAssembly – Components run directly in the browser via the .NET runtime. The
WebRenderer(src/Components/Web/src/WebRenderer.cs) applies diffs directly to the DOM through WebAssembly interop.
The ComponentBase class exposes the current hosting model through _renderHandle.RendererInfo, allowing components to adapt behavior when necessary while maintaining the same rendering logic across both environments.
Manual RenderTreeBuilder Usage
While Razor files generate BuildRenderTree automatically, developers can manually construct render trees for dynamic scenarios. The following example demonstrates imperative component construction equivalent to declarative Razor markup:
public class CounterManual : ComponentBase
{
private int currentCount;
private RenderFragment _fragment;
public CounterManual()
{
_fragment = builder =>
{
builder.AddMarkupContent(0, "<h3>Counter</h3>");
builder.OpenElement(1, "p");
builder.AddContent(2, $"Current count: {currentCount}");
builder.CloseElement();
builder.OpenElement(3, "button");
builder.AddAttribute(4, "onclick",
EventCallback.Factory.Create(this, Increment));
builder.AddContent(5, "Click me");
builder.CloseElement();
};
}
protected override void BuildRenderTree(RenderTreeBuilder builder) =>
_fragment(builder);
void Increment()
{
currentCount++;
StateHasChanged();
}
}
This pattern creates a RenderFragment that accepts a RenderTreeBuilder and uses sequence numbers (0, 1, 2...) to identify frames for diffing purposes. The OpenElement and CloseElement calls define HTML tags, while AddAttribute attaches event handlers through EventCallback.Factory.
Summary
- ComponentBase provides the lifecycle foundation and
StateHasChanged()implementation insrc/Components/Components/src/ComponentBase.cs. - RenderTreeBuilder constructs UI frames at
src/Components/Components/src/Rendering/RenderTreeBuilder.cswhileRenderTreeDiffBuildercomputes minimal updates. - StateHasChanged queues renders through the
RenderHandleonly when no pending render exists andShouldRender()returns true. - ShouldRender offers a customization point to suppress unnecessary renders for performance optimization.
- RemoteRenderer and WebRenderer provide hosting-specific implementations that apply the same diff output via SignalR or direct DOM manipulation respectively.
Frequently Asked Questions
What triggers a Blazor component to re-render?
A component re-renders when StateHasChanged() is called and the internal conditions in ComponentBase are satisfied: no render is already queued, and either the component hasn't rendered yet, ShouldRender() returns true, or a hot-reload operation is active. Event callbacks and parameter changes automatically trigger this notification.
How does Blazor minimize DOM updates?
Blazor minimizes DOM updates through the RenderTreeDiffBuilder algorithm in src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs. This compares the new render tree against the previous snapshot and computes only the differences. Instead of regenerating the entire UI, only changed attributes, text nodes, or element insertions/removals are transmitted and applied.
What is the difference between OnInitialized and OnParametersSet?
OnInitialized and OnInitializedAsync execute exactly once after component creation and initial parameter assignment, making them ideal for one-time setup logic. OnParametersSet and OnParametersSetAsync run every time the parent supplies new parameter values, including the initial assignment, allowing components to react to changing input values throughout their lifecycle.
Can you prevent a Blazor component from rendering?
Yes, by overriding ShouldRender() to return false. This protected virtual method in ComponentBase allows components to skip the render tree construction and diffing phases when state changes occur that don't require visual updates. This optimization prevents unnecessary CPU cycles and network traffic in Server-Side Blazor scenarios.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →