Or in other words, why you should hard-code sequence numbers, and not generate them programmatically.
Unlike .jsx
files, .razor
/.cshtml
files are always compiled. This is potentially a great advantage for .razor
, because we can use the compile step to inject information that makes things better or faster at runtime.
A key example of this are sequence numbers. These 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 simple .razor
file:
@if (someFlag)
{
<text>First</text>
}
Second
This compiles to something like the following:
if (someFlag)
{
builder.AddContent(0, "First");
}
builder.AddContent(1, "Second");
When this executes for the first time, if someFlag == true
, the builder receives:
Sequence | Type | Data |
---|---|---|
0 | Text node | First |
1 | Text node | Second |
Now imagine that someFlag
becomes false
, and we render again. This time the builder receives:
Sequence | Type | Data |
---|---|---|
1 | Text node | Second |
When it performs a diff, it sees that the item at sequence 0
was removed, so it generates the following trivial edit script:
- Remove the first text node
Imagine instead that you wrote the following rendertree builder logic:
var seq = 0;
if (someFlag)
{
builder.AddContent(seq++, "First");
}
builder.AddContent(seq++, "Second");
Now the first output would be:
Sequence | Type | Data |
---|---|---|
0 | Text node | First |
1 | Text node | Second |
... in other words, identical to before, so no issues yet. But then on the second render when someFlag==false
, the output would be:
Sequence | Type | Data |
---|---|---|
0 | Text node | Second |
This time, the diff algorithm will see that two changes have occurred, and will generate 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, and has resulted in a diff that is now twice as long.
This is a very trivial example. In more realistic cases with complex and deeply nested structures, and especially with loops, the performance cost is more severe still. Instead of immediately identifying which loop blocks or branches have been inserted/removed, the diff algorithm would have to recurse deeply into your trees and would build far longer edit scripts, because you're misleading it about how the old and new structures relate to each other.
- Q: Despite this, I still want to generate the sequence numbers dynamically. Can I?
- A: You can, but it will make your app performance worse.
- Q: Couldn't the framework make up its own sequence numbers automatically at runtime?
- A: No. The necessary information doesn't exist unless it is captured at compile time. Please see the example above.
- Q: I find it impractical to hard-code sequence numbers in really long blocks of manually-implemented RenderTreeBuilder logic. What should I do?
- A: Don't write really long blocks of manually-implemented RenderTreeBuilder logic. Preferably use
.razor
files and let the compiler deal with this for you, or if you can't, split up your code into smaller pieces wrapped inOpenRegion
/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 you want.
- A: Don't write really long blocks of manually-implemented RenderTreeBuilder logic. Preferably use
- Q: OK, so I'm going to hardcode the sequence numbers. What actual numeric values should I use?
- A: The diffing algorithm only cares that they should be an increasing sequence. It doesn't matter what initial value they start from, or whether there are gaps. One legitimate option would be to use the code line number as the sequence number. Or start from zero and increase by ones or hundreds or whatever interval you like.
- Q: Why does Razor Components use sequence numbers, when other tree-diffing UI frameworks don't?
- A: Because it makes diffing far faster, and Razor Components has the advantage of a compile step that deals with this automatically for people authoring
.razor
/.cshtml
files.
- A: Because it makes diffing far faster, and Razor Components has the advantage of a compile step that deals with this automatically for people authoring
So I'm having trouble with iterative loops (e.g.
for
andforeach
) and unnecessary renders, and I wonder if the sequence number has something to do with it.Suppose I have a loop like this:
I want to make sure that if the
People
collection changes, the list re-renders efficiently, i.e., I don't want everyPersonComponent
to have to re-render every time the collection changes. (AssumePersonComponent
executesStateHasChanged
when itsPerson
parameter changes).If I append to the end of
People
everything is fine. But if, for example, I insert aPerson
in front of the list, then all of the remainingPersonComponent
s re-render.The problem, I think I've determined, lies in the way Blazor is re-using the
PersonComponent
s. Rather than just insert a newPersonComponent
for the new person, and leave the rest of them alone, it's iterating through thePersonComponent
s created in the last render and assigning thePerson
s to them in list order.What I'm wondering is if there's a way around this through better use of the sequence number? When the list changes Is there a way to trick Blazor into reusing components so that they remain paired with their original list items whenever possible? Do we have any control at all over how Blazor reuses components?
I wonder if a robust solution to this really requires an
ItemsControl
paradigm a la WPF, wherein there a mechanism for associating stored visual elements with data and data - rather than sequence number - governs re-use of the visual. I'm not sure to what extent if any something like this can be implemented without going into the internals of Blazor. But without it I fear that very large collections with remotely complex visuals will have insurmountable performance issues.