Key Takeaways
- Blazor is a new single-page application (SPA) framework form Microsoft. Unlike other SPA frameworks such as Angular or React, Blazor relies on the .NET framework in favor of JavaScript.
- The Document Object Model (DOM) is a platform and language agnostic interface that treats an XML or HTML document as a tree structure. It allows for the document's content to be dynamically accessed and updated by programs and scripts.
- Blazor uses an abstraction layer between the DOM and the application code, called a RenderTree. It is a lightweight copy of the DOM's state composed by standard C# classes.
- The RenderTree can be updated more efficiently than the DOM and reconciles multiple changes into a single DOM update. To maximize effectiveness the RenderTree uses a diffing algorithm to to ensure it only updates the necessary elements in the browser’s DOM.
- The process of mapping a DOM into a RenderTree can be controlled with the @key directive. Controlling this process may be necessary in certain scenarios that require the context of different DOM elements to be maintained when a DOM is updated.
Blazor is a new single-page application (SPA) framework from Microsoft. Unlike other SPA frameworks such as Angular or React, Blazor relies on the .NET framework in favor of JavaScript. Blazor supports many of the same features found in these frameworks including a robust component development model. The departure from JavaScript, especially when exiting a jQuery world, is a shift in thinking around how components are updated in the browser. Blazor’s component model was built for efficiency and relies on a powerful abstraction layer to maximize performance and ease-of-use.
Abstracting the Document Object Model (DOM) sound intimidating and complex, however with modern web application it has become the normal. The primary reason is that updating what has rendered in the browser is a computationally intensive task and DOM abstractions are used to intermediate between the application and browser to reduce how much of the screen is re-rendered. In order to truly understand the impact Blazor’s RenderTree has on the application, we need to first review the basics.
Let’s begin with a quick definition of the Document Object Model. A Document Object Model or the DOM is a platform and language-agnostic interface that treats an XML or HTML document as a tree structure. In the DOM’s tree structure, each node is an object that makes up part of the document. This means the DOM is a document structure with a logical tree.
… the DOM is a platform and language agnostic interface that treats an XML or HTML document as a tree structure.
When a web application is loaded into the browser a JavaScript DOM is created. This tree of objects acts as the interface between JavaScript and the actual document in the browser. When we build dynamic web applications or single page applications (SPAs) with JavaScript we use the DOMs API surface. When we use the DOM for creating, updating and deleting HTML elements, modifying CSS and other attributes, this is known as DOM manipulation. In addition to manipulating the DOM, we can also use it and create and respond to events.
In the following code sample, we have a basic web page with two elements, an h1 and p. When the document is loaded by the browser a DOM is created representing the elements from the HTML. We can see in figure 1 a representation of what the DOM looks like as nodes in a tree.
<!DOCTYPE html>
<html>
<body>
<h1>Hello World</h1>
<p id="beta">This is a sample document.</p>
</body>
</html>
Figure 1: An HTML document is loaded as a tree of nodes; each object represents an element in the DOM.
Using JavaScript we can traverse the DOM explicitly by referencing the objects in the tree. Starting with the root node document we can traverse the objects children until we reach a desired object or property. For example, we can get the second child off the body branch by calling document.body.children[1] and then retrieve the innerText value as a property.
document.body.children[1].innerText
"This is a sample document."
An easier way to retrieve the same element is to use a function that will search the DOM for a specific query. Several convenience methods exist to query the DOM by various selectors. For example, we can retrieve the p element by its id beta using the getElementById
function.
document.getElementById("beta").innerText
"This is a sample document."
Throughout the history of the web, frameworks have made working with the DOM easier. jQuery is a framework that has an extensive API built around DOM manipulation. In the following example we’ll once again retrieve the text from the p element. Using jQuery’s $ method we can easily find the element by the id attribute and access the text.
//jQuery
$("#beta").text()
"This is a sample document."
jQuery’s strength is convenience methods which reduce the amount of code required to find and manipulate objects. However, the biggest drawback to this approach is inefficient handling of updates due to directly changing elements in the DOM. Since direct DOM manipulation is a computationally expensive task, it should be performed with a bit of caution.
Since direct DOM manipulation is a computationally expensive task, it should be performed with a bit of caution.
It’s common practice in most applications to perform several operations that update the DOM. Using a typical JavaScript or jQuery approach we may remove a node from the tree and replace it with some new content. When elements are updated in this way, elements and their children can often be removed and replaced when no change needed. In the following example, several similar elements are removed using a wildcard selector, n-elements. The elements are then replaced, even if they only needed modification. As we can see in figure 2, many elements are removed and replaced while only two required updates.
// 1 = initial state
$(“n-elements”).remove() // 2-3
$(“blue”).append(modifiedElement1) // 4
$(”green”).append(modifiedElement2) // 4
$(“orange”).append(modifiedElement3) // 4
Figure 2: 1) The initial state; 2) Like elements are selected for removal 3) Elements and their children are removed from the DOM 4) All elements are replaced with only some receiving changes.
In a Blazor application we are not responsible for making changes to the DOM. Instead, Blazor uses an abstraction layer between the DOM and the application code we write. Blazor’s DOM abstraction is called the RenderTree and is a lightweight copy of the DOM’s state. The RenderTree can be updated more efficiently than the DOM and reconciles multiple changes into a single DOM update. To maximize effectiveness the RenderTree uses a diffing algorithm to ensure it only updates the necessary elements in the browser’s DOM.
If we multiple updates on elements in a Blazor application within the same scope of work, the DOM will only receive the changes produced by the final difference produced. When we perform work a new copy of the RenderTree is created from the changes either through code or data binding. When the component is ready to re-render the current state is compared to the new state and a diff is produced. Only the difference values are applied to the DOM during the update.
Let’s look take a closer look at how the RenderTree can potentially reduce DOM updates. In figure 3 we begin with the initial state with three elements that will receive updates, green, blue, and orange.
Figure 3: The initial state of the RenderTree (left) and DOM (right). The elements with the values, green, blue, and orange will be affected by code.
In figure 4 we can see work being done over several steps within the same cycle. The items are removed and replaced, with the result swapping only the value of green and blue. Once the lifecycle is complete the differences are reconciled.
Figure 4: 1) our current RenderTree 2-4) some elements removed, replaced, and updated 5) The current state and new state are compared to find the difference.
Figure 5: The RenderTree difference is used to update only elements that changed during the operation.
Creating a RenderTree
In a Blazor application, Razor Components (.razor) are actually processed quite differently from traditional Razor Pages or Views, (.cshtml) markup. Razor in the context of MVC or Razor Pages is a one-way process that is rendered server-side as HTML. A component in Blazor takes a different approach - its markup is used to generate a C# class that builds the RenderTree. Let’s take a closer look at the process to see how the RenderTree is created.
When a Razor Component is created a .razor file is added to our project, the contents are used to generate a C# class. The generated class inherits from the ComponentBase class which includes the component’s BuildRenderTree method as shown in Figure 6. BuildRenderTree is a method receives a RenderTreeBuilder object and appends the component to the tree by translating our markup into RenderTree objects.
Figure 6: The ComponentBase class diagram with the BuildRenderTree method highlighted.
Using the Counter component example included in the .NET template we can see how the component’s code becomes a generated class. In the counter component there are significant items we can identify in the resulting RenderTree including:
- page routing directive
- h1 is a basic HTML element
- p, currentCount is a mix of static content and data-bound field
- button with an onclick event handler of IncrementCount
- code block with C# code
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
The code in the counter example is used to generate a class with detailed BuildRenderTree method that describes the objects in the tree. If we examine the generated class, we can see how the significant items were translated into pure C# code:
- page directive becomes an attribute tag on the class
- Counter is a public class that inherits ComponentBase
- AddMarkupContent defines HTML content like the h1 element
- Mixed elements such as p, currentCount become separate nodes in the tree defined by their specific content types, OpenElement and AddContent
- button includes attribute objects for CSS and the onclick event handler
- code within the code block is evaluated as C# code
[Route("/counter")]
public class Counter : ComponentBase
{
private int currentCount = 0;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddMarkupContent(0, "<h1>Counter</h1>\r\n\r\n");
builder.OpenElement(1, "p");
builder.AddContent(2, "Current count: ");
builder.AddContent(3, this.currentCount);
builder.CloseElement();
builder.AddMarkupContent(4, "\r\n\r\n");
builder.OpenElement(5, "button");
builder.AddAttribute(6, "class", "btn btn-primary");
builder.AddAttribute<MouseEventArgs>(7, "onclick",
EventCallback.Factory.Create<MouseEventArgs>(this,
new Action(this, Counter.IncrementCount)));
builder.AddContent(8, "Click me");
builder.CloseElement();
}
private void IncrementCount()
{
this.currentCount++;
}
}
We can see how the markup and code turn into a very structured piece of logic. Every part of the component is represented in the RenderTree so it can be efficiently communicated to the DOM.
Included with each item in the render tree is a sequence number, ex: AddContent(num, value). Sequence numbers are included to assist the diffing algorithm and boost efficiency. Having a raw integer gives the system an immediate indicator to determine if a change has happened by evaluating order, presence or absence of and item’s sequence number. For example, if we compare a sequence of objects 1,2,3 with 1,3 then it can be determined that 2 is removed from the DOM.
The RenderTree is a powerful utility that is abstracted away from us by clever tooling. As we can see by the previous examples, our components are just standard C# classes. These classes can be built by hand using the ComponentBase class and manually writing the RenderTreeBuilder method. While possible, this would not be advised and is considered bad practice. Manually written RenderTree’s can be problematic if the sequence number is not a static linear number. The diffing algorithm needs complete predictability, otherwise the component may re-render unnecessarily voiding it’s efficiently.
Manually written RenderTree’s can be problematic if the sequence number is not a static linear number.
Optimizing Component Rendering
When we work with list of elements or components in Blazor we should consider how the list of items will behave and the intentions of how the components will be used. Ultimately Blazor's diffing algorithm must decide how the elements or components can be retained and how RenderTree objects should map to them. The diffing algorithm can generally be overlooked, but there are cases where you may want to control the process.
- A list rendered (for example, in a @foreach block) which contains a unique identifier.
- A list with child elements that may change with inserted, deleted, or re-ordered entries
- In cases when re-rendering leads to visible behavior differences, such as lost element focus.
The RenderTree mapping process can be controlled with the @key directive attribute. By adding a @key we instruct the diffing algorithm to preserve elements or components related to the key's value. Let’s look at an example where @key is needed and meets the criteria listed above (rules 1-3).
An unordered list ul is created. Within each list item li is an h1 displaying the Value of the class Color. Also, within each list item is an input which displays a checkbox element. To simulate work that we might do in a list such as: sorting, inserting, or removing items, a button is added to reverse the list. The button uses an in-line function items = items.Reverse() to reverse the array of items when the button is clicked.
<ul class="list-group">
@foreach (var item in items)
{
<li class="list-group-item">
<h1>@item.Value</h1>
<input type="checkbox" />
</li>
}
</ul>
<button @onclick="_ => items = items.Reverse()">Reverse</button>
@code {
// class Color {int Id, string Value}
IEnumerable<Color> items = new Color[] {
new Color {Id = 0, Value = "Green" },
new Color {Id = 1, Value = "Blue" },
new Color {Id = 2, Value = "Orange" },
new Color {Id = 3, Value = "Purple" }
};
}
When we run the application the list renders with a checkbox for each item. If we select the checkbox in the “Green” list item then reverse the list, then the selected checkbox will remain at the top of the list and is now occupying the “Purple” list item. This is because the diffing algorithm only updated the text in each h1 element. The initial state, and reversed state is shown in Figure 30, note the position of the checkbox remains unchanged.
Figure 7: A rendering error is visible as the checkbox fails to move when the array is reversed, and the DOM loses context of the element’s relationship.
We can use the @key directive to provide additional data for the RenderTree. The @key will identify how each list item is related to its children. With this extra information the diffing algorithm can preserve the element structure. In our example we’ll assign the item’s Id to the @key and run the application again.
@foreach (var item in items)
{
<li @key="item.Id" class="list-group-item">
<h1>@item.Value</h1>
<input type="checkbox" />
</li>
}
With the @key directive applied the RenderTree will create, move, or delete items in the list and their associated child elements. If we select the checkbox in the “Green” list item then reverse the list, then the selected checkbox will also move because the RenderTree is moving the entire li group of elements within the list, this can be seen in Figure 31.
Figure 8: Using the key attribute the elements retain their relationship and the checkbox remains with the appropriate container as the DOM updates.
For this example, we had an ideal scenario that met the criteria for needing @key. We were able to fix the visual errors caused by re-rendering the list of items. However, use cases aren’t always this extreme, so it’s important to take careful consideration and understand the implications of applying @key.
When @key isn't used, Blazor preserves child element and component instances as much as possible. The advantage to using @key is control over how model instances are mapped to the preserved component instances, instead of the diffing algorithm selecting the mapping. Using @key comes with a slight diffing performance cost, however if elements are preserved by the RenderTree it can result in a net benefit.
Conclusion
While the RenderTree is abstracted away through the Razor syntax in .razor files, it’s important to understand how it impacts the way we write code. As we saw through example, understanding the RenderTree and how it works is essential when writing components that manage a hierarchy. The @key attribute is essential when working with collections and hierarchy so the RenderTree can be optimized and avoid rendering errors.
About the Author
Ed Charbeneau is the author of the Free Blazor E-book; Blazor a Beginners Guide, a Microsoft MVP and an international speaker, writer, online influencer, a Developer Advocate for Progress, and expert on all things web development. Charbeneau enjoys geeking out to cool new tech, brainstorming about future technology, and admiring great design.