My Blazor component tree is connected to the business logic of the running application and represents its current state. There are a few event handlers (@onclick
) and these directly call into the business logic to have an effect there. Any change of state in the business logic in turn is pushed to the Blazor components and cause them to rerender.
All the event handlers in the Blazor components are "pure" in the sense that they never change any state in the component. The current implementation of Blazor components though unconditionally re-renders (StateHasChanged) after the event callback has run even though this really isn't necessary for my components. A bit worse: The components are rendered with the old state after the event handler and then immediately the new state from the business logic arrives and causes a render for the new state.
I pretty much look for a way to avoid this StateHasChanged
call.
Currently I'm avoiding the rerender caused by this call by managing a shouldRender
flag within my component and toggle it depending on what happens:
protected override void OnParametersSet() => shouldRender = true;
protected override bool ShouldRender()
{
if (shouldRender)
{
shouldRender = false;
return true;
}
else
return false;
}
or
InvokeAsync(() =>
{
shouldRender = true;
StateHasChanged();
shouldRender = false;
});
But this code is brittle, not very intuitive and hard to link to the reason why it's there, because I have to put it on the non-event side of things.
I would like to be able to specify this conditionally. Something like the following would be great.
@onmousemove:preventStateHasChanged
@onmousemove:preventStateHasChanged=true
@onmousemove:preventStateHasChanged=SomeCSharpFieldOrMethod()
We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.
Please consider implementing norender
by removing the reference to this
in the EventCallback. This enables storing RenderFragment with event callbacks in static variables: #24655
@SteveSandersonMS I very often run into performance issues that boil down to unnecessary render cycles introduced by the implicit render on @bind
and events. Could this be worth tackling as part of the perf improvements for .NET 5? As a library author, it would be much easier to use a norender
flag on event callbacks and tell users to do the same, than to try and work around the rendering with making ShouldRender
return false ever so often.
@mkArtakMSFT Please add the "Help Wanted" label so someone can pick it up in a sprint :-)
I've created a sample repo that demonstrates the overhead quite clearly: https://github.com/stefanloerwald/blazor-bind-overhead
On SSB, the delay is just about noticable, on CSB there's no way you'll miss it.
Before someone says this example is contrived: Of course it is. Nobody will ever want 60000 buttons next to each other, even if 59999 are not rendered. The markup of the child node is deliberately simplistic. Note though that when the child component is more complex, the delay would increase too, making the delay noticable with fewer children. More complex scenarios are e.g. tables (data grids), diagrams, charts, etc.
A demo for CSB can be found here: https://stefanloerwald.github.io/blazor-bind-overhead/
StateHasChanged
causing first a render with old values followed by one with new values. As you can see from #24599 this causes misbehaviour with two way bound components.I have also been running into unwanted rerenders when using binding and event handlers, would love to be able to control the behaviour without having to rely on ShouldRender.
@danroth27, @SteveSandersonMS
Any chance you could please give this some attention? In terms of performance, this has quite the potential to make an impact.
I've just updated the demo page and I think it shows that the issue is quite severe. Please have a look at https://stefanloerwald.github.io/blazor-bind-overhead/ and corresponding repo. The overhead per component is around 0.28ms upwards (higher overhead for more complex markup, of course, even if the resulting render diff is entirely empty).
As I was asked on a different channel about the seemingly low number of 0.28ms: Yes, this is the correct unit and the decimal point is in the right place.
However, this isn't a low figure at all, considering that this is per component for the only effect of determining that the render diff is empty.
In terms of keeping applications smooth, one could consider the target of 60fps. With about 60-100 tiny components re-rendered without any visual change, 16ms are easily wasted by re-rendering nothing. So doing anything meaningful on top of that means the actual FPS drops below 60.
So in conclusion: 0.28ms isn't much on its own, but it adds up quickly to noticable delays.
Wow ... this is a shock. I'm from a xamarin background. I was listening to someone from an Angular background saying how poorly blazor performs ... and I was starting to believe him because comparing a xamarin app on an iphone to a blazor app on a hefty azure server .. the mobile app wins hands down!
I have a list of 120 cards on my page each with information about a staff member. Pushing one button to update one card causes the getters on every field on every card to be called. It takes 2 second. That's just poor architecture.
You get to control the granularity of change-detection and rerendering by choosing how to split up your UI into components:
ShouldRender
to provide custom logic to say whether the child should re-render.So if you want the rendering not to recurse into a particular subtree, you can either ensure you're only passing primitive parameter types (e.g., int
, string
, DateTime
) so the framework can detect if there are definitely no mutations, or in more complex scenarios you can override ShouldRender
and plug in your own logic. This is almost identical to the update flow used very successfully by React.
@SteveSandersonMS Consider a scenario where a component has the following
1: A parameter passed by its parent
2: An @onclick
event
3: An @onmousemove
event (which we don't want to cause a render)
The smallest amount of code I can think of is
bool PreventRender = false;
private void MyOnMouseMove()
{
PreventRender = true;
}
protected override bool ShouldRender()
{
if (!PreventRender)
return true;
PreventRender = false;
return false;
}
Or instead of being imperative, we could be declarative and write one of the following
@onmousemove:preventStateHasChanged
@onmousemove:preventStateHasChanged=SomeCSharpFieldOrMethod()
I prefer declarative anyway, but in this case it's also a lot less work.
I don't think the PreventRender
pattern that @mrpmorris shows is even sufficient to cover all cases. When you're designing a component LibraryComponent
where the users of that component provide other child elements with bindings to that component, there's no way to get this working. For example
<LibraryComponent>
@foreach (var item in elements)
{
<UserComponentHere @key="item" @bind-Value="@item.X" />
}
</LibraryComponent>
Suppose the LibraryComponent
needs to re-render when elements
change, but shouldn't (because of high cost) when any of the bindings fire, there's no way within the LibraryComponent
to make that happen. If I could tell my users to use @bin-Value:norender="@item.X"
, then problem solved.
Most helpful comment
@SteveSandersonMS Consider a scenario where a component has the following
1: A parameter passed by its parent
2: An
@onclick
event3: An
@onmousemove
event (which we don't want to cause a render)The smallest amount of code I can think of is
Or instead of being imperative, we could be declarative and write one of the following
I prefer declarative anyway, but in this case it's also a lot less work.