In my previous post, I laid out the fundamentals of what can cause a Blazor site to be slow due to re-rendering. By far the most common cause of such slowness is when hundreds or thousands of components all need to re-render multiple times based on changes to parameters or explicit calls to StateHasChanged()
. Often it is the case that only a few of the component instances in question truly need to re-render, but Blazor has no way to avoid such re-rendering, so it is up to the developer to worry about this concern whenever dealing with a proliferation of component instances.
The parent component
This potential slowness is often manifest in lists, grids, or tables where the content of each item is non-trivial. Imagine a UI with a grid of product images: a ProductGrid.razor something like…
<button @onclick="Add">Add new to top</button>
<ul>
<Virtualize Context="productViewModel" Items="Products">
<ProductTile Product="productViewModel" @key="productViewModel.Code" />
</Virtualize>
</ul>
@code {
[Parameter, EditorRequired]
public List<ProductViewModel> Products { get; set; } = null!;
private void Add() => Products.Insert(0, new());
}
Note here we have used the Virtualize component to avoid rendering components for items in the List that aren’t currently shown (due to scrolling). We are sure also to use a @key
attribute on the root child element or component inside Virtualize.
So this parent component seems pretty simple and already optimized using Virtualize; how could it be a problem?
Narrow edits can cause wide re-renders
Note there is a button to add a new empty product. When this button is clicked, Blazor re-renders the home component, which means every child component will re-render as well. In our case, this probably means our ProductGrid and the 50+ shown ProductTiles, and suppose each of which has a dozen small components it shows and thus has to rerender. So that’s easily 770+ rerenders needed just to show the new item.
Ideally, when clicking the add button, there would only be a re-render of the ProductGrid, exactly one ProductTile (the new one), and all the small components (but only those in that one instance): thus 14 re-renders.
The reason each of the ProductTiles has to rerender is that its parent is rerendering, and it takes a ProductTileViewModel (one of our domain classes) as a parameter. Blazor cannot determine that nothing has changed in the fifty shown products, as it cannot interrogate all the contents of ProductTileViewModel and compare them to the previous value–remember, Blazor automatically avoids rerendering based on values only when all parameters are of certain well-known immutable or primitive types.
The main way to address this in the ProductTile component is to override OnParametersSet to detect to store the previous values for every part of the parameter that affects the displayed state, and override ShouldRender
on the ProductTile component so that it will return false when the previous values are all the same as the current values.
But such logic comparing “previous to current” would have to be repeated in every component, yet customized for each one to ensure you are tracking all the properties you care about. I’ll avoid showing you the example code for this since it is not the way I recommend doing it.
Avoiding the re-renders
So here’s where my Nuget package Sz.BlazorRenderReducers comes into play. Using its DisplayHashRerenderComponentBase and implementing the protected override string DisplayHash
get-only property, I can simply return a string that is unique for each display state that is possible; usually, this is just going to be a string that contains the value of every property that affects the display state in any way.
This has the benefit of reducing the code required to finely control re-renderings to two lines of code in most cases: one line to inherit from DisplayHashRerenderComponentBase and one line to override the DisplayHash getter. So, such a ProductTile.razor will look something like:
@using Sz.BlazorRerenderReducers
@inherits DisplayHashRerenderComponentBase
<li class="product-tile">
<ProductImage Product="@Product" />
<div>
@if (!Product.IsInStock)
{
<OutOfStockBadge />
}
<ProductCode Product="@Product" />
<ProductPriceTag Product="@Product" />
</div>
</li>
@code {
[Parameter, EditorRequired]
public ProductViewModel Product { get; set; } = null!;
// when this value is unchanged, supresses unnecessary re-rendering
protected override string? GetDisplayHash() =>
$"{Product.Code};{Product.Price};{Product.IsInStock}";
// write a console line on every render
protected override void OnAfterRender(bool _) =>
Console.WriteLine($"Rendered {GetType()}");
}
Here, the DisplayHash get-only property can return anything that is uniquely determined by the data that affects what could be rendered by the component. True to its name, it is a hash for the display of the component. In this example, I’ve just shown a simple concatenation of the values that influence the display, but you could have any function here you’d like – perhaps using hash functions implemented on the parameters themselves, or serializing them using System.Text.Json, but I have found often a interpolated string is simplest and easiest to understand. The base component DisplayHashRerenderComponentBase will then properly store the previous and current values of this hash, and ShouldRender will return true or false appropriately, removing the need to reimplement such custom ShouldRender code in each component.
Drawbacks
Of course, one drawback to this approach is that it requires that all such components must inherit from DisplayHashRerenderComponentBase, thus removing the possibility of inheriting from any other base component, abstract or otherwise (unless of course you can in turn make that base component inherit from DisplayHashRerenderComponentBase). Fortunately, I have not seen much need to inherit a component from anything other than ComponentBase (which DisplayHashRerenderComponentBase itself inherits from), so this has not yet been an issue for me.
If you’ve followed along to this point, perhaps you can imagine the bigger drawback to this approach: it requires that the developer specify every potential source of changes to the display state. In effect, you have to “register” (by including in the DisplayHash getter logic) every property that might affect the GUI in each component, including its children components (after all. if a parent component avoids re-rendering, there’s nothing to cause its children to re-render either). As you can imagine, this can lead to confusion as to why a component isn’t re-rendering when it needs to–sometimes parameter properties are used but forgotten to be added to the DisplayHash. No error results and no cue is possible to tell the developer that they forgot to tweak the DisplayHash because of a new binding or some other new code indirectly affecting a bound reference.
However, even this latter drawback seems lower-risk than the frequent errors and confusion that results from manually making every component have a ShouldRender method that would itself need to take into account every relevant property and also would need to track the current and old values of each such property in order to determine if a render is necessary. So this technique seems to be a distinct improvement over having such nuanced and perhaps repetitive (though custom) ShouldRender overrides in many components.
Demo
So try it now on this demo page. To the code from above, I’ve added a toggle checkbox at the top to enable and disable the technique. We can open the console an d add an item, you’ll see a console line was written on each ProductTile render. With GetDisplayHash implemented, we’ve reduced the number of ProductTile renders from dozens to one–but don’t forget the child components that are unchanged: we’ve reduced the total number of components that render from several hundred to five. In my experience, this often cuts the “lag” after the user clicks the button from a few seconds to imperceptibly fast. On apps where users do lots of navigation or small manipulations, lag of a few seconds can kill your perceived performance, so this is a big win.
So if you have (or suspect you have) hundreds or thousands of unnecessary renders happening on many changes, give Sz.BlazorRerenderReducers a try to help drastically reduce the re-rendering that is needed. You can read more on the BlazorRerenderReducers project GitHub page and leave an issue with any concerns or questions, or just reply to this post.
Interesting, this reminds me of WPF and MVVM a little, except in reverse; here, every property notifies the UI when it changes, thus updating it, whereas in MVVM you need to specify for every single property if it should notify the UI. Which makes me wonder if your solution couldn't be the basis for some kind of Blazor MVVM.
ReplyDelete