Testing

webTiger Logo Wide

.NET Blazor Performance Recommendations

Blazor Logo

Blazor is a .NET-based Component Architecture platform that is offered as a potential alternative to other modern frameworks such as Angular or ReactJS for responsive web development.

If you are familiar with other Component Architecture frameworks and are also a .NET developer then moving to Blazor should be fairly straightforward. But, there are some caveats and design constraints you should consider in Blazor that you might not worry about in other frameworks.

In this article:

Preamble

UI Component Architecture design aims to break front-ends (UIs) into a hierarchy of small, and largely independent, parts that can be combined to build a UI. A key tenet of the pattern is reuse, where components may be used many times in different pages/screens of the app.

Components are typically broken down into four types:

  • Atoms: low-level, basic components such as buttons, labels, input boxes, etc.
  • Molecules: simple structures such as a form field (i.e. a label, input box, and tool-tip).
  • Cells: more complicated layouts such as forms (i.e. a collection of form fields with Submit and Cancel buttons).
  • Bodies/Pages: A page or screen of the UI, comprised of cells, molecules, and atoms.

A single page could therefore comprise of 100s or even 1000s of individual component instances.

This number of components becomes very significant in Blazor if we aren’t careful as the performance recommendations and design diversions from standard component architecture practices in this article will demonstrate.

This content is largely based on Microsoft’s own Blazor Performance Recommendations, but adds more demonstrative examples, and additional lessons learnt on Blazor development projects I’ve completed.

Checklist

TLDR; if you don’t have time to read the full article, here’s a bullet-pointed checklist of the main design and performance considerations:

  • Rendering modes can be configured to better meet requirements and performance goals.
  • Use conditional rendering to reduce rendering task complexity.
  • Cache previous state and use that to only re-render components if changes have occurred (by comparing to the current state), as this can dramatically reduce overall rendering time.
  • Flatten complex per-record component hierarchies when rendering lots of records, otherwise app performance could dramatically suffer.
  • Limit the number of parameters used per component.
  • Manually assign parameters in Blazor components wherever possible instead of relying on Blazor’s default mechanism that uses Reflection.
  • Consider using virtualisation (i.e. the <Virtualize> component) when rendering collections of records so that only those components in the current viewport are rendered and displayed.
  • Adopt use of strongly typed model classes supplied as a single parameter to components, instead of a set of individual parameters, to improve rendering performance.
  • When working with JS-Interops, try to pre-serialise any complex objects to JSON, because performance can degrade exponentially as more complex serialisation/deserialisation is handled automatically by the JS-Interop API itself.
  • Casting of IJSRuntime to IJSInProcessRuntime on Blazor WebAssembly projects can boost JS-Interop performance when working with synchronous JS functions.
  • Do not call StateHasChanged() unnecessarily. Only call it when re-rendering is required (e.g. when component state has changed).
  • Apply throttling to potential rapidly occurring event instances (like the mouse move event) to reduce re-rendering overhead when processing events and using the JS-Interop API.
  • Avoid defining and processing Lambda expresions in razor markup files.
  • When working in .NET 7+, use the [JSImport] and [JSExport] attribute tags on JS-Interop methods to allow compile-time linking instead of runtime linking using Reflection (which would otherwise be the default behaviour).

Rendering Modes

With the release of .NET 8, Blazor introduced explicit rendering modes in an attempt to optimise rendering performance.

Rendering mode can be defined at various levels within the app’s components hierarchy, from app-wide assignment, to page-by-page definition, or even on a component-by-component basis. Using the rendering mode most suitable for individual components can significantly improve rendering performance.

The following rendering modes are supported:

  • [Blazor Server] Static SSR (Server-Side Rendering). In this mode, content is fully rendered server-side, and the web response returned to the browser contains the markup to display. This is similar to traditional server-side rendering models like ASP.NET and MVC.
  • [Blazor Server] Interactive SSR. In this mode, a lightweight rendering of content is performed server-side and the web response is returned. At the same time a WebSockets session is established between client and server through which server-side event handlers are wired up, dynamic content may be downloaded, etc. This makes the app more responsive and dynamic due to its connected state, supporting features such as push notifications and server-side triggers. The lighter-weight initial render can result in faster load performance compared to Static SSR, but at the expense of the page possibly not being fully functional or missing content when it is first displayed on the client.
  • [Blazor WebAssembly] Interactive CSR (Client-Side Rendering). In this mode, Blazor initially renders a lightweight representation of content and displays it, and wires up client-side event handlers afterwards.
  • [Blazor Server] Interactive Auto. Blazor uses Interactive SSR on the initial render and then CSR on subsequent renders once the client-side bundle has been downloaded.

By default, unconfigured Blazor Server code projects will adopt Static SSR, and Blazor WebAssembly code projects will adopt Interactive CSR.

There are hierarchy rules that apply to rendering modes. For example, a parent component configured with Interactive SSR cannot have a child configured with Static SSR, because the child would need to be fully rendered while the parent only requires lightweight rendering before being displayed. The converse scenario is acceptable though: if the parent component is being staticly rendered server-side, then any children can be still rendering interactively if required.

Aside: Blazor Hybrid apps. With .NET 7, Microsoft introduced the concept of Blazor Hybrid apps. This allows native apps (MAUI, WPF and WinForms app projects are supported) to use their per-platform implementation of BlazorWebView to locally render Razor components in the app.

Rendering Mode Recommendations

These recommendations apply only to Blazor Server projects, as WebAssembly projects will always adopt Interactive CSR rendering.

  • For simple web apps or where initial page load time is less important than the page content being fully rendered with all data loaded, use Static SSR.
  • Where page loading times are paramount, use interactive rendering and background load content/data once the page has been displayed, and optionally showing work-in-progress notifications or spinners temporarily until the data can be displayed.

Component Rendering

Component re-rendering in Blazor can be quite expensive in performance terms so it is best to ensure rendering is only performed when absolutely necessary.

Consider the following example, where we’re displaying a messaging history in an app:

// Message.cs (model class)
public class Message
{
    public int Id { get; set; } = 0;
    public string Summary { get; set; } = "";
    public string Content { get; set; } = "";
    public DateTime Sent { get; set; } = DateTime.Now;
}

// MessagingHistory.razor
<div class="ex-message-history-viewer">
    @foreach (Message message in History)
    {
        <MessageDetails Message="message" 
                        ToggleExpandCollapse="@HandleToggleContentVisibility"
                        IsExpanded="@(ExpandedMessageIds.Contains(message.Id))" />
    }
</div>
@code {
    [Parameter]
    public IEnumerable<Message> History { get; set; } = new List<Message>();

    private List<int> ExpandedMessageIds { get; } = new();

    private void HandleToggleContentVisibility(int messageId)
    {
        if (ExpandedMessageIds.Contains(messageId))
        {
            ExpandedMessageIds.Remove(messageId);
        }
        else 
        {
            ExpandedMessageIds.Add(messageId);
        }
    }
}

// MessageDetails.razor
<div class="ex-message-details">
    <div class="ex-message-title-bar">
        <div class="ex-message-title">@Message.Summary</div>
        <div class="ex-message-time">@($"{Message.Sent:HH:mm:ss dd MMM yyyy}")</div>
        <div class="ex-content-expander @(IsExpanded ? "active" : "")"
             @onclick="HandleToggleExpanded">
        </div>
    </div>
    <div class="ex-message-content-pane">
        <MessageContent Content="@Message.Content" />
    </div>
</div>
@code {
    [Parameter]
    public Message Message { get; set; } = new();

    [Parameter]
    public EventCallback<int> ToggleExpandCollapse { get; set; }

    [Parameter]
    public bool IsExpanded { get; set; } = false;

    private void HandleToggleExpanded()
    {
        ToggleExpandCollapse.InvokeAsync(Message.Id);
    }
}

// MessageContent.razor
<div class="ex-message-body @(IsExpanded ? "" : "hidden")">
    @Content
</div>
@code {
    [Parameter]
    public string Content { get; set; } = "";

    [Parameter]
    public bool IsExpanded { get; set; }
}Code language: HTML, XML (xml)

Conditional Rendering

In the above example, if our messaging history has 100s, 1000s, or even more entries then rendering could take a very long time to complete. We’re also rendering the content section even if it is being hidden by the ‘hidden’ CSS class.

Basic Conditional Rendering Opportunities

Many UI elements will consist of multiple components. When developing such hierarchies of components, consider whether selective sub-trees need to be rendered at all.

For example, we could change the MessageContent component like so:

// MessageContent.razor
@if (IsExpanded)
{
    <div class="ex-message-body">
        @Content
    </div>
}
@code {
    [Parameter]
    public string Content { get; set; } = "";

    [Parameter]
    public bool IsExpanded { get; set; }
}Code language: HTML, XML (xml)

Now, when an instance of MessageDetails is rendered, content is only rendered in the MessageContent sub-component if that region is expanded.

But that still initialises the MessageContent sub-component instance itself – it is only the content within it that isn’t being rendered now.

Also, when dealing with large datasets, it is prudent to consider paging/windowing the data so rendering overhead is greatly reduced.

So, we could refactor our components like this instead:

// MessagingHistory.razor
<div class="ex-message-history-viewer-container">
    <div class="ex-message-history-viewer">
        @foreach (Message message in InView)
        {
            <MessageDetails Message="message"
                            ToggleExpandCollapse="@HandleToggleContentVisibility"
                            IsExpanded="@(ExpandedMessageIds.Contains(message.Id))" />
        }
    </div>
    <div class="ex-pager">
        <button type="button" class="ex-pager-button" @onclick="PreviousPage">
            Previous
        </button>
        <span class="ex-pager-page-number">@PagesToSkip</span>
        <button type="button" class="ex-pager-button" @onclick="NextPage">
            Next
        </button>
    </div>
</div>
@code {
    [Parameter]
    public IEnumerable<Message> History { get; set; } = new List<Message>();

    private List<int> ExpandedMessageIds { get; } = new();

    private const int PageSize = 10;
    private int PagesToSkip { get; set; } = 0;
    private IEnumerable<Message> InView { get; set; } = new List<Message>();

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        InView = PageSize > 0
            ? History.Skip(PagesToSkip * PageSize).Take(PageSize).ToList()
            : History.ToList();
    }

    private void NextPage()
    {
        if (PageSize < 1) return;

        if (PagesToSkip < Math.Ceiling(History.Count() / (decimal)PageSize))
        {
            PagesToSkip++;
            InView = History.Skip(PagesToSkip * PageSize).Take(PageSize);
        }
    }

    private void PreviousPage()
    {
        if (PageSize < 1) return;

        if (PagesToSkip < 2) PagesToSkip = 1;
        else PagesToSkip--;

        InView = History.Skip(PagesToSkip * PageSize).Take(PageSize);
    }

    private void HandleToggleContentVisibility(int messageId)
    {
        if (ExpandedMessageIds.Contains(messageId))
        {
            ExpandedMessageIds.Remove(messageId);
        }
        else
        {
            ExpandedMessageIds.Add(messageId);
        }
    }
}

// MessageDetails.razor
<div class="ex-message-details">
    <div class="ex-message-title-bar">
        <div class="ex-message-title">@Message.Summary</div>
        <div class="ex-message-time">@($"{Message.Sent:HH:mm:ss dd MMM yyyy}")</div>
        <div class="ex-content-expander @(IsExpanded ? "active" : "")"
            @onclick="HandleToggleExpanded">
        </div>
    </div>
    <div class="ex-message-content-pane">
        @if (IsExpanded)
        {
            <MessageContent Content="@Message.Content" />
        }
    </div>
</div>
@code {
    [Parameter]
    public Message Message { get; set; } = new();

    private bool IsExpanded { get; set; } = false;

    private void HandleToggleExpanded()
    {
        ToggleExpandCollapse.InvokeAsync(Message.Id);
    }
}

// MessageContent.razor
<div class="ex-message-body">
    @Content
</div>
@code {
    [Parameter]
    public string Content { get; set; } = "";
}Code language: HTML, XML (xml)

Now, only the messages currently in view are rendered and, for each message, the content component is not rendered at all unless the message pane is expanded by the user.

Deciding When Components Should Render

By default, Blazor component trees re-render due to any state changes. If the expand/collapse button on a single MessageDetails component instance is toggled, should the runtime be re-rendering all other components on the current view? (If you didn’t appreciate why all of them might re-render, it would be because state in the top-level MessagingHistory component has changed due to the toggle.)

Aside: the example code was developed in this way for demonstration purposes, so that message visibility toggling would trigger changes in the parent component. Good component design would typically put control of the message content visibility toggling in the MessageDetails component itself, where visibility changes would only affect the state of that component and likely limit re-rendering to it.

Assuming we don’t want to refactor the code to move where management of the expanded/collapsed state is stored, let’s make changes to the MessageDetails component so it holds a copy of its previous state and can evaluate if new state data really represents state changes.

// MessageDetails.razor

<div class="ex-message-details">
    <div class="ex-message-title-bar">
        <div class="ex-message-title">@Message.Summary</div>
        <div class="ex-message-time">@($"{Message.Sent:HH:mm:ss dd MMM yyyy}")</div>
        <div class="ex-content-expander @(IsExpanded ? "active" : "")"
             @onclick="HandleToggleExpanded">
        </div>
    </div>
    <div class="ex-message-content-pane">
        @if (IsExpanded)
        {
            <MessageContent Content="@Message.Content" />
        }
    </div>
</div>
@code {
    [Parameter]
    public Message Message { get; set; } = new();

    [Parameter]
    public EventCallback<int> ToggleExpandCollapse { get; set; }

    [Parameter]
    public bool IsExpanded { get; set; } = false;

    private Dictionary<string, object> _lastState = new();
    private bool _shouldRender = true;

    protected override void OnParametersSet()
    {
        _shouldRender = this.Message != null &&
            (!_lastState.ContainsKey(nameof(this.Message)) ||
            !_lastState.ContainsKey(nameof(IsExpanded)) ||
            _lastState[nameof(this.Message)] != this.Message ||
            ((Message)_lastState[nameof(this.Message)]).Id != this.Message.Id ||
            ((Message)_lastState[nameof(this.Message)]).Summary != this.Message.Summary ||
            ((Message)_lastState[nameof(this.Message)]).Sent != this.Message.Sent ||
            ((Message)_lastState[nameof(this.Message)]).Content != this.Message.Content ||
            (bool)_lastState[nameof(IsExpanded)] != IsExpanded);

        _lastState.TryAdd(nameof(this.Message), this.Message ?? new Message());
        _lastState.TryAdd(nameof(IsExpanded), IsExpanded);
    }

    protected override bool ShouldRender() => _shouldRender;

    private void HandleToggleExpanded()
    {
        ToggleExpandCollapse.InvokeAsync(Message.Id);
    }
}Code language: HTML, XML (xml)

Notice that we’ve added a couple of fields to cache state and control rendering, and overridden the OnParametersSet() and ShouldRender() methods.

Now on the initial render of the component, the state is cached, and on subsequent state changes the current and cached (i.e. previous) states are compared. Re-rendering then only occurs if the data is materially different. We’ve dramatically reduced the overall rendering overhead involved in toggling visibility of a single message in the view.

Virtualisation

If you are unfamiliar with Razor component virtualisation then please read Microsoft’s documentation on it first as an explainer.

Virtualisation can be considered conditional rendering to a certain extent, but it merits its own section here to help explain the subtleties of its implementation and use.

Blazor provides a Virtualize component out-of-the-box that can be used with lists of records and will conditionally render only those components for which records are in the current viewport. This can be useful where paging isn’t preferred or maybe unsuitable (i.e. the user doesn’t want to page through data due to the individual item layout and instead wants to scroll down through records for a better user experience (UX)).

To ‘virtualise’ a component’s sub-tree all we need to do is use the Virtualize component instead of a loop statement when iterating through records that we are displaying. For example, we could replace the loop section of our MessagingHistory component like this:

<div class="ex-message-history-viewer-container">
    <div class="ex-message-history-viewer">
        <Virtualize Items="@History" Context="message">
        
            <MessageDetails @key="message.Id"
                            Message="message"
                            ToggleExpandCollapse="@HandleToggleContentVisibility"
                            IsExpanded="@(ExpandedMessageIds.Contains(message.Id))" />
        </Virtualize>
    </div>
</div>Code language: HTML, XML (xml)

The pager controls aren’t required now and the full messaging history collection can be passed into the Virtualize component as it will decide what gets rendered automatically.

So, if its all as simple as this then why was it given its own section in this article?

Well, as previously mentioned there are a few caveats and subtleties to consider when using it.

Firstly, the Virtualize component works to a fixed per-item height. By default this is 50px but it can be changed in the component’s attributes as required. A consequence of expecting fixed height child components is that if your items are being displayed by components with varying heights (e.g. if there are different classes of item, with some common and some unique data properties) then it may be difficult for the user to navigate to specific component instances within the control using the vertical scroll bar slider. What can happen in this case is that the slider control’s thumb can jump around alarmingly.

Another common problem is where child components are initially the same height but have collapsible sub-sections. A consequence of expecting fixed height child components is that if your items are being displayed by components with expandable/collapsible regions then as sub-sections are expanded or collapsed the rest of the components being displayed, the overall layout, and even navigation may be affected.

JavaScript Interoperability

When interacting with client-side JavaScript routines in Blazor, performance can suffer significantly if the correct working practices are not adhered to.

For asynchronous operations, perform fewer large operations instead of lots of smaller, atomic ones. There is an inherit latency and processing overhead in executing requests through Blazor’s JS Runtime so keeping this to a minimum is a good thing.

Blazor may suffer exponential performance degradation as the size of data objects being passed through the JS Runtime increase. Even when data is only a few kilobytes performance can become unmanageable and the app may freeze or become unusable. If passing larger objects to the client-side JavaScript routines really is necessary then consider pre-serialising the data to JSON on the .NET code side and re-hydrating them back from JSON in JavaScript if required, as this can improve performance a lot compared to blindly passing .NET objects into the Blazor JS Runtime.

When performing synchronous JavaScript operations in a Blazor WebAssembly app, consider casting the IJSRuntime service instance to IJsInProcessRuntime instead so Blazor doesn’t have the overhead of wrapping calls in requests, promises, etc.

if using .NET 7+, use the [JSImport] and [JSExport] to provide compile-time linking of JS interops instead of the traditional runtime linking that is otherwise employed. Microsoft have worked had in recent versions of .NET to provide better performance when using Reflection by offering some compile-time handling via such attributes, and I suspect that is the case here too.

Component Parameters Performance Considerations

Number of Parameters

By default, when parameter data is passed into a component, Blazor uses .NET Reflection to process them. Refection is typically a slow process and Microsoft themselves have estimated that any additional parameter may increase rendering time by up to 15ms – and that’s per-component instance. For example, if we had 1000 instances of a component being rendered, and that component accepted 10 parameters then we’d potentially have a 150 second delay (1000 x 10 x 15ms) before rendering completed.

Microsoft recommend a single parameter and use of a custom model class (like in our Messages History example code above) instead of individual parameters wherever possible.

Consider Manually Processing Parameters

To improve parameter processing performance in components, consider explicitly processing parameter data passed into the component instead of relying on Blazor’s default handling. Fortunately, Blazor provides base class methods to make this easy. For example:

@code {
    [Parameter]
    public int Id { get; set; }
    
    [Parameter]
    public string FirstName { get; set; }

    [Parameter]
    public string LastName { get; set; }

    [Parameter]
    public string Username { get; set; }

    protected override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (Parameter parameter in parameters)
        {
            switch (parameter.Name)
            {
                case nameof(Id):
                    Id = (int)parameter.Value;
                    break;
                case nameof(FirstName):
                    FirstName  = (string)parameter.Value;
                    break;
                case nameof(LastName):
                    LastName = (string)parameter.Value;
                    break;
                case nameof(Username):
                    Username = (string)parameter.Value;
                    break;
                default:
                    // Ignore unrecognised parameters.
                    break;
            }
        }

        return await base.SetParametersASync(ParameterView.Empty);
    }
}Code language: C# (cs)

The above completely bypasses Blazor’s default parameters assignment mechanism (which is slow because it uses Reflection) and performs direct identification and assignment of parameter values. Using the nameof expression won’t impact performance as this is converted at compile time to a string value.

Capturing Unmatched Values

Avoid capturing unmatched values in parameters except as a means of last resort. By default this is disabled on Blazor components but can be enabled via the [Parameter] attribute if necessary.

// AVOID USING!
[Parameter(CaptureUnmatchedValues = true)]
public Message Content { get; set; }Code language: C# (cs)

Enabling this option causes Blazor to maintain a collection of non-matching parameter name-value pairs and decide how values should overwrite one another, and all this on top of the overhead of processing the parameters using Reflection already.

Diverging from Component Architecture Best Practice

Traditional UI Component Architecture design principles and guidelines often extol the virtual of lots of small components that build into a hierarchy of fewer larger ones, often quoting SOLID design principles when objects should do one-thing and we should be separating concerns.

While that’s good general advice, experienced UI designers should weigh those principles against how a particular programming API or development platform behaves.

We already know there’s often an inherit rendering overhead per-component in Blazor. So, why make that worse by nesting several levels of child components unnecessarily, with the immediate performance cost that implies?

Often, inlining child content in the same component in Blazor can improve rendering performance dramatically compared to a hierarchy of components in a sub-tree. For example, if we revisit our messaging history example above, while it does follow good design practice to break our UI components down like we did, we could just as easily have done this:

<div class="ex-message-history-viewer">
    <Virtualize Items="@History" Context="message">
        <div class="ex-message-details">
            <div class="ex-message-title-bar">
                <div class="ex-message-title">@message.Summary</div>
                <div class="ex-message-time">@($"{message.Sent:HH:mm:ss dd MMM yyyy}")</div>
            </div>
            <div class="ex-message-content-pane">
                <div class="ex-message-body">
                    @message.Content
                </div>
            </div>
        </div>
    </Virtualize>
</div>

@code {
    [Parameter]
    public List<Message> History { get; set; } = new();
}Code language: HTML, XML (xml)

Suddenly we’ve got a flat component structure with minimal code. We’ve thrown away the ability to expand/collapse the content section here, but since we’re using the Virtualize component too, that probably reduces our app behaviour headaches anyway.

Adopt Re-usable Render Fragment Where Possible

Render fragments allow us to define repeatable markup in one place and use it multiple times in a component. This reduces rendering overhead as the fragment is rendered once regardless of how many times it is displayed on a view.

Render fragments can be defined at component level or more widely throughout the app as the need arises. For example:

// SharedFragments.cs
internal static class SharedFragments
{
    public static RenderFragment ArticleComponentTitle = @<h2>News Article</h2>;
}


// GossipArticle.razor
@SharedFragments.ArticleComponentTitle
<div>
    @foreach (NewsSection section in Article.Sections)
    {
        <div class="ex-article-section">
            <h3>@section.Title</h3>
            @Disclaimer
            <div>@section.Content</div>
        </div>
    }
</div>
@code {
    [Parameter]
    public NewsArticle Article { get; set; }

    private RenderFragment Disclaimer = 
        @<p class="disclaimer">The content in this section may not be accurate.<p>
}Code language: HTML, XML (xml)

We have defined a shared render fragment that can be used by multiple components to provide a common title for news article viewers, and we’ve provided a component-specific fragment that is used multiple times in a particular article.

Above we specified a ‘gossip article’ which needed a disclaimer highlighting the potential inaccuracy of the information being displayed. We could have another component for verified news and re-use the shared render fragment there so the title is consistent across components:

// VerifiedNewsArticle.razor
@SharedFragments.ArticleComponentTitle
<div>
    @Disclaimer
    @foreach (NewsSection section in Article.Sections)
    {
        <div class="ex-article-section">
            <h3>@section.Title</h3>
            <div>@section.Content</div>
        </div>
    }
    @Disclaimer
</div>
@code {
    [Parameter]
    public NewsArticle Article { get; set; }

    private RenderFragment Disclaimer = 
        @<p class="disclaimer">The content in this article has been verified.<p>
}Code language: HTML, XML (xml)

We’ve been able to re-use the standardised title fragment and developed a different layout for our verified news articles.

Avoid Calling StateHasChanged Unnecessarily

Most of the time Blazor components are able to detect state changes automatically and re-render as needed. In scenarios where this isn’t happening, the framework provides a StateHasChanged method to give the executional runtime a nudge.

It is easy to get into the habit of calling this method all over the place to improve confidence that any state changes are being picked up, but it should be used with caution as ‘updates’ might just be setting the same value and so explicitly triggering re-renders is just adversely affecting performance.

Selectively Apply Throttling To Rapidly Occurring Events

There may be cases where input-based events, such as when the user is moving the mouse and the app is tracking it, fire many times in a short period. For example, the mouse-move event could be raised for every single pixel change in position of the mouse pointer.

Handling every single event instance in this case probably won’t add much value to the user or app behaviour and will just put unnecessary load on host resources. If event handling is spanning the JS Runtime API boundary then this is just likely to exacerbate problems and introduce further performance bottlenecks.

In these cases, consider throttling in event handling routines so they ignore event instances that occur too closely to a previous one, and so they don’t spam the app with requests. Most events are likely to be coming from client-side JavaScript code through the Blazor JS Runtime API, so it would make sense to throttle them in the JavaScript code itself, wherever possible, instead of letting them propagate at all.

A JS library like loadash can provide a simple mechanism for applying throttling without any programming headaches. For example, this single line of code registers an on-scroll event handler throttled so it only processes events every 100ms:

jQuery(container).on('scroll', _.throttle(positionChanged, 100));Code language: JavaScript (javascript)

Limit Use of Lambda Expressions in Markup

Within components, there is an inherent performance overhead when processing Lambda expressions declared in attributes in markup compared to declaring them in the code-behind section.

In the following code, we demonstrate the poor use of Lambda expressions in the markup and also how we can perform the same behaviours in the code-behind:

// Bad practice: Lambda expression used in markup.
<div>
    @foreach (Message message in Messages)
    {
        string id = message.Id;
        <MessageDetails Message="@message" Delete="@(e => HandleDelete(e, id))" />
    }
</div>
@code {
    [Parameter]
    public List<Message> Messages { get; set; }

    // Delete handler method omitted for brevity.
}

// Good practice: Lambda expression in code-behind.
<div>
    @foreach (DeleteableMessage message in Messages)
    {
        string id = message.Id;
        <MessageDetails Message="@message" Delete="@message.DeleteHandler" />
    }
</div>
@code {
    public class DeleteableMessage : Message
    {
        public Action DeleteHandler { get; set; } = (e) => { };
    }

    [Parameter]
    public List<DeleteableMessage> Messages { get; set; }

    protected override void OnInitialized()
    {
        foreach (DeleteableMessage message in Messages)
        {
            int id = message.Id;
            message.DeleteHandler = (e) => 
            {
                DeleteMessage(id);
            }
        }
    }

    // DeleteMessage method omitted for brevity.
}Code language: HTML, XML (xml)

Use 3rd Party Libraries With Caution

It is always tempting to use a 3rd party library or toolkit stuffed full of already developed components to save lots on development time/effort. Two obvious toolkits to mention here are MudBlazor and Telerik, and I’ve used both of them.

While it is true that using those toolkits have accelerated UI development for me a lot in the past, and they are both worth considering for that, I have experienced problems and shortcomings with both of them too.

The trouble is, that once you’re locked into using them and start discovering functionality or performance issues in those 3rd party components as your use cases become more complex it can be difficult to clearly identify root cause (your code or their code). If it’s the 3rd party code then it may be slow/difficult, or completely out of your control to fix.

With Mudblazor, I struggled with the limited Grid/Tabular layout support as, at the time of writing, the toolkit only offered a fixed 12-point grid for page layout (i.e. not for data display) and a flat 2-D table component. For more complex datasets, that meant a lot of manual coding to get nested tabular views the way we wanted, or even having to abandon Mudblazor’s components completely and development our own components from scratch.

By comparison, Telerik provides a great fully-featured data grid that I’d previously used in ASP.NET MVC, and loved, but the Blazor grid component became almost unworkable as we tried to use it beyond very simple implementations. The grid became a performance killer as soon as you threw more than a few records at it. Since it was rendering a custom component of mine per record we did the due diligence of developing our own alternative to the grid, and this immediately fixed our performance issues and so we abandoned use of Telerik’s grid ever since.

I’ve also struggled with consistently overriding styles on Telerik components in both ReactJS and Blazor at times, despite not having had this problem on ASP.NET MVC.

There’s no doubt 3rd party component libraries can save a lot of programming effort but it is also worth considering whether those toolkits are fit for purpose on a project-by-project basis. Often development teams will establish a design constraint to prefer use of a toolkit comprehensively on all projects just because they have or are paying for it, instead of considering whether it is the best fit for a specific use case.