Skip to content

Commit 2672a33

Browse files
authored
Split Razor Components topic (dotnet#16990)
1 parent 8cb36c8 commit 2672a33

22 files changed

Lines changed: 1383 additions & 1311 deletions
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
---
2+
title: ASP.NET Core Blazor advanced scenarios
3+
author: guardrex
4+
description: Learn about advanced scenarios in Blazor, including how to incorporate manual RenderTreeBuilder logic into an app.
5+
monikerRange: '>= aspnetcore-3.1'
6+
ms.author: riande
7+
ms.custom: mvc
8+
ms.date: 02/12/2020
9+
no-loc: [Blazor, SignalR]
10+
uid: blazor/advanced-scenarios
11+
---
12+
# ASP.NET Core Blazor advanced scenarios
13+
14+
By [Luke Latham](https://github.com/guardrex) and [Daniel Roth](https://github.com/danroth27)
15+
16+
## Manual RenderTreeBuilder logic
17+
18+
`Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder` provides methods for manipulating components and elements, including building components manually in C# code.
19+
20+
> [!NOTE]
21+
> Use of `RenderTreeBuilder` to create components is an advanced scenario. A malformed component (for example, an unclosed markup tag) can result in undefined behavior.
22+
23+
Consider the following `PetDetails` component, which can be manually built into another component:
24+
25+
```razor
26+
<h2>Pet Details Component</h2>
27+
28+
<p>@PetDetailsQuote</p>
29+
30+
@code
31+
{
32+
[Parameter]
33+
public string PetDetailsQuote { get; set; }
34+
}
35+
```
36+
37+
In the following example, the loop in the `CreateComponent` method generates three `PetDetails` components. When calling `RenderTreeBuilder` methods to create the components (`OpenComponent` and `AddAttribute`), 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](#sequence-numbers-relate-to-code-line-numbers-and-not-execution-order) section.
38+
39+
`BuiltContent` component:
40+
41+
```razor
42+
@page "/BuiltContent"
43+
44+
<h1>Build a component</h1>
45+
46+
@CustomRender
47+
48+
<button type="button" @onclick="RenderComponent">
49+
Create three Pet Details components
50+
</button>
51+
52+
@code {
53+
private RenderFragment CustomRender { get; set; }
54+
55+
private RenderFragment CreateComponent() => builder =>
56+
{
57+
for (var i = 0; i < 3; i++)
58+
{
59+
builder.OpenComponent(0, typeof(PetDetails));
60+
builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
61+
builder.CloseComponent();
62+
}
63+
};
64+
65+
private void RenderComponent()
66+
{
67+
CustomRender = CreateComponent();
68+
}
69+
}
70+
```
71+
72+
> [!WARNING]
73+
> 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.
74+
75+
### Sequence numbers relate to code line numbers and not execution order
76+
77+
Razor component files (*.razor*) are always compiled. Compilation is potentially a great advantage over interpreting code because the compile step can be used to inject information that improves app performance at runtime.
78+
79+
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.
80+
81+
Consider the following Razor component (*.razor*) file:
82+
83+
```razor
84+
@if (someFlag)
85+
{
86+
<text>First</text>
87+
}
88+
89+
Second
90+
```
91+
92+
The preceding code compiles to something like the following:
93+
94+
```csharp
95+
if (someFlag)
96+
{
97+
builder.AddContent(0, "First");
98+
}
99+
100+
builder.AddContent(1, "Second");
101+
```
102+
103+
When the code executes for the first time, if `someFlag` is `true`, the builder receives:
104+
105+
| Sequence | Type | Data |
106+
| :------: | --------- | :----: |
107+
| 0 | Text node | First |
108+
| 1 | Text node | Second |
109+
110+
Imagine that `someFlag` becomes `false`, and the markup is rendered again. This time, the builder receives:
111+
112+
| Sequence | Type | Data |
113+
| :------: | ---------- | :----: |
114+
| 1 | Text node | Second |
115+
116+
When the runtime performs a diff, it sees that the item at sequence `0` was removed, so it generates the following trivial *edit script*:
117+
118+
* Remove the first text node.
119+
120+
### The problem with generating sequence numbers programmatically
121+
122+
Imagine instead that you wrote the following render tree builder logic:
123+
124+
```csharp
125+
var seq = 0;
126+
127+
if (someFlag)
128+
{
129+
builder.AddContent(seq++, "First");
130+
}
131+
132+
builder.AddContent(seq++, "Second");
133+
```
134+
135+
Now, the first output is:
136+
137+
| Sequence | Type | Data |
138+
| :------: | --------- | :----: |
139+
| 0 | Text node | First |
140+
| 1 | Text node | Second |
141+
142+
This outcome is identical to the prior case, so no negative issues exist. `someFlag` is `false` on the second rendering, and the output is:
143+
144+
| Sequence | Type | Data |
145+
| :------: | --------- | ------ |
146+
| 0 | Text node | Second |
147+
148+
This time, the diff algorithm sees that *two* changes have occurred, and the algorithm generates the following edit script:
149+
150+
* Change the value of the first text node to `Second`.
151+
* Remove the second text node.
152+
153+
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.
154+
155+
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 has to recurse deeply into the render trees. This usually results in having to build longer edit scripts because the diff algorithm is misinformed about how the old and new structures relate to each other.
156+
157+
### Guidance and conclusions
158+
159+
* App performance suffers if sequence numbers are generated dynamically.
160+
* The framework can't create its own sequence numbers automatically at runtime because the necessary information doesn't exist unless it's captured at compile time.
161+
* 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.
162+
* 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).
163+
* 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.

0 commit comments

Comments
 (0)