Aspnetcore: Add a way to avoid component re-rendering caused by events to Blazor components ("pure event handlers")

Created on 10 Feb 2020  路  15Comments  路  Source: dotnet/aspnetcore

Is your feature request related to a problem? Please describe.

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.

Describe the solution you'd like

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.

affected-most area-blazor enhancement severity-minor

Most helpful comment

@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.

All 15 comments

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/

24599 looks like it has similar issues to this. Specifically about 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:

  • When an event occurs, the associated component re-renders
  • When any component re-renders, its immediate children are re-rendered if (and only if) their parameter values may have changed. Or you can override 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.

Was this page helpful?
0 / 5 - 0 ratings