ASP.NET Core Blazor advanced scenarios (render tree construction)

Note

This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.

Warning

This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 version of this article.

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 9 version of this article.

This article describes the advanced scenario for building Blazor render trees manually with RenderTreeBuilder.

Warning

Use of RenderTreeBuilder to create components is an advanced scenario. A malformed component (for example, an unclosed markup tag) can result in undefined behavior. Undefined behavior includes broken content rendering, loss of app features, and compromised security.

Manually build a render tree (RenderTreeBuilder)

RenderTreeBuilder provides methods for manipulating components and elements, including building components manually in C# code.

Consider the following PetDetails component, which can be manually rendered in another component.

PetDetails.razor:

<h2>Pet Details</h2>

<p>@PetDetailsQuote</p>

@code
{
    [Parameter]
    public string? PetDetailsQuote { get; set; }
}

In the following BuiltContent component, the loop in the CreateComponent method generates three PetDetails components.

In RenderTreeBuilder methods with a sequence number, sequence numbers are source code line numbers. The Blazor difference algorithm relies on the sequence numbers corresponding to distinct lines of code, not distinct call invocations. When creating a component with RenderTreeBuilder methods, hardcode the arguments for sequence numbers. Using a calculation or counter to generate the sequence number can lead to poor performance. For more information, see the Sequence numbers relate to code line numbers and not execution order section.

BuiltContent.razor:

@page "/built-content"

<PageTitle>Built Content</PageTitle>

<h1>Built Content Example</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent() => CustomRender = CreateComponent();
}
@page "/built-content"

<PageTitle>Built Content</PageTitle>

<h1>Built Content Example</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent() => CustomRender = CreateComponent();
}
@page "/built-content"

<h1>Build a component</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent()
    {
        CustomRender = CreateComponent();
    }
}

Warning

The types in Microsoft.AspNetCore.Components.RenderTree allow processing of the results of rendering operations. These are internal details of the Blazor framework implementation. These types should be considered unstable and subject to change in future releases.

Sequence numbers relate to code line numbers and not execution order

Razor component files (.razor) are always compiled. Executing compiled code has a potential advantage over interpreting code because the compile step that yields the compiled code can be used to inject information that improves app performance at runtime.

A key example of these improvements involves sequence numbers. Sequence numbers indicate to the runtime which outputs came from which distinct and ordered lines of code. The runtime uses this information to generate efficient tree diffs in linear time, which is far faster than is normally possible for a general tree diff algorithm.

Consider the following Razor component file (.razor):

@if (someFlag)
{
    <text>First</text>
}

Second

The preceding Razor markup and text content compiles into C# code similar to the following:

if (someFlag)
{
    builder.AddContent(0, "First");
}

builder.AddContent(1, "Second");

When the code executes for the first time and someFlag is true, the builder receives the sequence in the following table.

Sequence Type Data
0 Text node First
1 Text node Second

Imagine that someFlag becomes false and the markup is rendered again. This time, the builder receives the sequence in the following table.

Sequence Type Data
1 Text node Second

When the runtime performs a diff, it sees that the item at sequence 0 was removed, so it generates the following trivial edit script with a single step:

  • Remove the first text node.

The problem with generating sequence numbers programmatically

Imagine instead that you wrote the following render tree builder logic:

var seq = 0;

if (someFlag)
{
    builder.AddContent(seq++, "First");
}

builder.AddContent(seq++, "Second");

The first output is reflected in the following table.

Sequence Type Data
0 Text node First
1 Text node Second

This outcome is identical to the prior case, so no negative issues exist. someFlag is false on the second rendering, and the output is seen in the following table.

Sequence Type Data
0 Text node Second

This time, the diff algorithm sees that two changes have occurred. The algorithm generates the following edit script:

  • Change the value of the first text node to Second.
  • Remove the second text node.

Generating the sequence numbers has lost all the useful information about where the if/else branches and loops were present in the original code. This results in a diff twice as long as before.

This is a trivial example. In more realistic cases with complex and deeply nested structures, and especially with loops, the performance cost is usually higher. Instead of immediately identifying which loop blocks or branches have been inserted or removed, the diff algorithm must recurse deeply into the render trees. This usually results in building longer edit scripts because the diff algorithm is misinformed about how the old and new structures relate to each other.

Guidance and conclusions

  • App performance suffers if sequence numbers are generated dynamically.
  • The necessary information doesn't exist to permit the framework to generate sequence numbers automatically at runtime unless the information is captured at compile time.
  • Don't write long blocks of manually-implemented RenderTreeBuilder logic. Prefer .razor files and allow the compiler to deal with the sequence numbers. If you're unable to avoid manual RenderTreeBuilder logic, split long blocks of code into smaller pieces wrapped in OpenRegion/CloseRegion calls. Each region has its own separate space of sequence numbers, so you can restart from zero (or any other arbitrary number) inside each region.
  • If sequence numbers are hardcoded, the diff algorithm only requires that sequence numbers increase in value. The initial value and gaps are irrelevant. One legitimate option is to use the code line number as the sequence number, or start from zero and increase by ones or hundreds (or any preferred interval).
  • For loops, the sequence numbers should increase in your source code, not in terms of runtime behavior. The fact that, at runtime, the numbers repeat is how the diffing system realises you're in a loop.
  • Blazor uses sequence numbers, while other tree-diffing UI frameworks don't use them. Diffing is far faster when sequence numbers are used, and Blazor has the advantage of a compile step that deals with sequence numbers automatically for developers authoring .razor files.