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:

  1. Component instantiation – The framework creates an instance of the component class, typically derived from ComponentBase.
  2. InitializationOnInitialized and OnInitializedAsync execute once after the component receives its initial parameters.
  3. Parameter assignment – When parent components supply new parameter values, OnParametersSet and OnParametersSetAsync run to allow state recalculation.
  4. Render tree construction – The component's BuildRenderTree method invokes RenderTreeBuilder methods to emit the frame sequence.
  5. Diff calculation – The new render tree is compared against the previous version by RenderTreeDiffBuilder, located in src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs. Only the minimal set of changes is computed.
  6. Commit phase – Changes are applied to the actual DOM in WebAssembly mode, or serialized and sent via SignalR to a RemoteRenderer in Server-Side Blazor.
  7. Post-render callbacksOnAfterRender and OnAfterRenderAsync fire after the UI updates complete, providing access to firstRender boolean 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() returns true, 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 in src/Components/Components/src/ComponentBase.cs.
  • RenderTreeBuilder constructs UI frames at src/Components/Components/src/Rendering/RenderTreeBuilder.cs while RenderTreeDiffBuilder computes minimal updates.
  • StateHasChanged queues renders through the RenderHandle only when no pending render exists and ShouldRender() 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →