When you think about it, data-bound real-time DOM updates are weird. We update values; then other code that we didn’t call knows somehow to call our code, then our libraries update the DOM based on those results. In Blazor, React, and Vue, it all just seems to work–but sometimes there is a nagging slowness to sites that bites our users on every click. What causes this? Usually, it is slowness in rendering.
This is a Blazor post, but first let me quickly contrast Blazor against the JavaScript framework/libraries React and Vue, which both have a robust “reactivity” implementation. In Vue, references for data binding are exposed in each component’s data and computed properties. Then, the magic of Vue automatically injects code using JavaScript’s prototype system so that on every set, code is run that causes every relevant getter to rerun, thus causing Vue’s internal representation of the DOM (called the “virtual DOM”) to update itself from the relevant data in memory. This is automatically followed by the internal code that updates the real DOM from the virtual DOM and then allows the browser to repaint the screen. This is actually quite elegant and it is all hidden from the developer. React does something very similar.
But in Blazor, there is no setter injection, as C# does not provide this capability natively. So instead, the Blazor component base classes internally rely on a simpler but less elegant approach: anytime the parameters to a component change, all the properties and methods that are bound in that component, and recursively in all its child components, will run. Since ideally the parameters to a component uniquely determine the values used therein and therefore determine the DOM for that component, this is seemingly good enough: everything updates immediately. Microsoft’s Blazor docs call this process “rendering” the component. We consider “rendering” in Blazor not to include the time it takes for the browser to paint this DOM, which is seldom (but not never) significant.
However, this can result in at least two pitfalls. First, the time to render (again, think “run C# code for my component”) can indeed be significant. In theory, I thought this would not be a problem; after all, hypothetical C# code run via the CLR (for example, in a unit test or command-line app) would take nanoseconds, and even if there were thousands of components on a page, we’re still talking microseconds here. The problem with this theory is that Blazor code run via WebAssembly isn’t run via the CLR, and so the time it takes a unit test to run a fragment of your component’s code is not the same as it will take a browser to run it. In fact, this difference can be small or it can be 100x or 1000x, and there isn’t always a good way to predict where this slowness will come. (In particular, I’ve noticed that deserializing is slow.) However, there is no simple way to profile Blazor WebAssembly yet, so it can be tough to isolate this kind of rendering slowness.
Secondly, when properties of parameters may have changed but didn’t, Blazor will unnecessarily re-render that component and all its children. This often causes lots of unnecessary re-renders; for example, consider when a property is set to the same value it already has. Blazor cannot look within user-defined objects to see that nothing has changed, so it presumes that something may have changed and it triggers a re-render. A corollary to this: unnecessary child re-renders will happen when a parent component re-renders even when the parameters to the child haven’t changed. Imagine updating the title of a large table when none of the cells have changed. C# can’t tell if this might have impacted a child, so it plays it safe by re-rendering all children anytime there is a chance that changed data affects the DOM rendered by the children.
So why is everybody having such a great time with Blazor WebAssembly and seemingly not complaining of these re-renders? Thankfully, Microsoft already has reduced the impact of these re-renders as much as can be expected. First, Blazor is pretty dang fast. Your site may be doing hundreds or thousands of re-renders in one second without you realizing it. Perhaps it’s bad, but just not bad enough for you to have noticed. Just add the following…
protected override OnAfterRender() =>
Console.WriteLine($"Rendered {GetType()}");
…in all relevant components and see how much they re-render. You might be surprised at all the plate-spinning going on under the hood. Paradoxically, the speed of Blazor WebAssembly masks the theoretical slowness, and this theoretical slowness tends to grow as development of a site progresses till it becomes actual slowness as experienced by users.
Also mitigating re-render slowness is that, if a component’s parameters are entirely known primitive types (such as bool, int, decimal, string, DateTime, and Guid), then Blazor will cut off the re-rendering when the values haven’t changed. So, if you can make a component depend on a few strings or bools rather than one instance of a user-defined type (be it a class, struct, or record), do so.
Another good practice to mitigate the re-rendering penalty is to ensure that rendering or parameter changing does not repeatedly cause loading data from a web service or any other asynchronous operation. Structure your C# component code so that data is loaded only when detecting a change that requires it–often, when the previous value differs from the current value–rather than simply loading that data every time in OnParametersSetAsync
or OnAfterRenderAsync
. Also, do not bind the invocation of any such asynchronous method directly to a displayed element (e.g. <div>@(GetNameFromServer())</div>
); instead, bind to a property and some other event should invoke the method call to populate that property, usually on user action or based on conditional logic within a lifecycle event handler. In short, assume your component will be re-rendered many thousands of times, and the mere act of re-rendering should not do any heavy lifting except when something relevant has changed. Do not make components that require parent components to carefully limit re-rendering.
Is frequent re-rendering causing a long list, table, or other repeated components to bog you down? Most loops in your Blazor markup can be replaced by instances of the <Virtualize>
component. This component is a godsend: it will render only the iterations needed to provide a good user experience. The component often is a no-brainer drop-in replacement for a @foreach
loop, so learn it and use it often.
Finally, Blazor provides the protected override bool ShouldRender()
method. If you have some logic where you can decide when a rerender is necessary, just implement ShouldRender
to return false in the cases. Don’t worry about coding the first render as a special case: ShouldRender
doesn’t get called on the very first render of a component, so you don’t need to add code to cover the first render case. (Also note that if ShouldRender returns false, then child components will also not be rerendered.)
Even with all these partial solutions, you may well find yourself in the situation where you’ve optimized all you can and many components are still re-rendering too often. How do you reduce unnecessary renders when dealing with non-primitive parameters without adding lots of non-intuitive, custom complexity in ShouldRender
? I’ll have a solution for this in my next post.
No comments:
Post a Comment