DoLare
DoLare

Reputation: 83

Suggestions on how to redesign Blazor server-side app to be more responsive

I am struggling with responsiveness of our server-side app. I am sure there's something inherently bad with my design and cannot figure it out. I am providing a sample razor page for this post which is a simplified version of my application.

There is a typeahead (blazored typeahead control by Chris Sainty) control for rooms list and a button to join a room. Users will basically select a room from the typeahead control and click the join button. There is a also a section in the same page where I display all the rooms that user has joined in a table control.

Typeahead's room list is populated using a database query.

public async Task JoinRoom()
{
 //this adds the row to database table using async operation.
}

List<Rooms> GetAvailableRooms()
{
   //this is a synchronous operation
   var t = context.Rooms.ToList();
   return t;
}

List<Rooms> GetMyRooms()
{
   //this is a synchronous operation that returns list of user rooms
}


razor:

.
.
<RoomSelectorControl Operation="GetAvailableRooms" />
.
.
@if (SelectedRoom != null)
{
   <div class="col-sm-1">
     <div class="form-group">
       <button type="button" class="btn btn-info" @onclick="JoinRoom">&nbsp;Join&nbsp;</button>
     </div>
   </div>
}

var myRoomsList = GetMyRooms();
foreach (Room myRoom in myRoomsList)
{
    //displays the list in a table control
}

Also, I have two razor pages 1) rooms page (above) 2) seats page.

My issue is whenever I load rooms page or switch between pages and end up in rooms page, I am noticing unresponsive UI until both the rooms typeahead control and "my rooms" section is displayed. On top of that, rooms list and "my rooms" section is built from scratch every single time. I think that is also part of the issue.

So my questions:

  1. How can I redesign my code so that I initialize typeahead control's data only once per the lifetime of the application or at least browser session.
  2. What can I do to build "my rooms" section only once and then refresh it only when they click "Join" button?

Upvotes: 0

Views: 676

Answers (1)

Nik FP
Nik FP

Reputation: 3063

There is a lot of meat surrounding your 2 part question, but I'll give it a go. This shaped up to be a bit long, fair warning.

The first point to make is that you should keep in mind that Blazor Server Side doesn't work like traditional in-browser javascript applications. Everything the user does is being communicated back to the server through a SignalR connection, and the server then needs to process it on a UI thread, calculate a DOM diff, and send the diff back up the wire. If you are doing any synchronous operations, those run on the same thread, so if you have a blocking DB or API call going on at the server end, this will force your UI to lock up and wait for the thread to complete the long running procedure on the back end. To get around this, you can use asynchronous programming techniques to free up anything that is expected to take any amount of time from the UI thread. I'll apply this in a minute.

The second thing to consider is how you are handling concurrency in the app. Are there multiple users, and can data that is added, modified, or deleted by one user affect what should be populated in the lists? You may have already thought about this but it's worth considering.

So to your first point of the UI lockup, I'll make some assumptions on your property names but you should get the point.

First, your list backing field.

public List<Room> AllRooms { get; set; } = new List<Room>();

This will initialize your list ahead of the application running and avoid needing a null check for this item.

Then, let's change your method to a Task so it can be awaited, which will apply in a minute when we execute it to load your data.

Task<List<Rooms>> GetMyRooms()
{
   //this is a synchronous operation that returns list of user rooms
}

Now to load up your data. You mention that you want to restrict the data load to either once per browser session or once per application lifetime. I'll stick with the browser session example since if the application is running for a long time on the server (hours, days, weeks) and serving multiple users, you could end up with stale data fairly easily and I assume MyRooms is unique to each user.

Either way you'll want to take advantage of the Blazor Lifecycle methods, and for this one we'll use the OnAfterRenderAsync() method.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if(firstRender)
    {
        // await loading of rooms from DB or other persistance, 
        // then assign result to AllRooms property
        AllRooms = await GetAvailableRooms();

        // Call this only if the list doesn't populate after GetAvailableRooms 
        // completes, which it might not. 
        StateHasChanged()
    }
}

The big takeaway on the above is that the initialization logic only runs once when the page is loaded, after the first render. This gets the page awake and responsive and then loads data. You could also use the OnInitializedAsync method and have the first line in the body be await Task.Delay(1) which would break initialization free from the UI thread and allow the UI to finish rendering, however the initialized methods get called twice on page load, so you'd have 2 calls running to your data store. For this reason I tend to prefer the way I did it above, i.e. load a static initial state that will respond, then load the data.

Building the list for MyRooms will work in a similar manner. Change the call to a task, have it update a backing property in the OnAfterRenderAsync method, then call StateHasChanged at the end of the initialization if needed.

So now you are asking "What about loading the data multiple times?" Well, for that you can look toward service injection. For this, we'll use a scoped service which has a default lifetime of the life of the SignalR circuit, which is suitable for your needs.

So add something like the following class to your application:

public class RoomsService
{

    public RoomsService(//whatever you need to inject)
    {
        // Attach injected items to backing fields
    }

    // Whatever entity access you are using, initialized through constructor DI
    private Entity _entity;

    // Flags to determine if items need load from entity store
    private bool allRoomsInitialLoad = true;
    private bool myRoomsInitialLoad = true;

    // Backing list of all rooms
    private List<Room> _rooms; 

    public List<Room> GetAllRooms()
    {
        if(!allRoomsInitialLoad )
            return _rooms;

        _rooms = _entity.WhateverMethodGetstheRooms();
        allRoomsInitialLoad = false;
        return _rooms;
    }

    // Backing list of my rooms
    private List<Room> _myRooms; 

    public List<Room> GetMyRooms(int userId)
    {
        if(!myRoomsInitialLoad)
            return _myRooms;

        _rooms = _entity.WhateverMethodGetsMyRoomsById(userId);
        myRoomsInitialLoad = false;
        return _myRooms;
    }

    public List<Room> JoinRoom(int roomId, int userId)
    {
        _entity.WhateverMethodAddsYouToRoomByIds(roomId, userId);
        var room _entity.GetRoomById(roomId);
        _myRooms.Add(room);
        return _myRooms;
    }
}

The gist of this service is that the public methods will only call the persistence layer for rooms on first load, otherwise they will return an in-memory representation of the persistance layer list. You may need to check this against concurrency concerns for stale data but that is up to you.

Let's add it to the Startup.cs services collection:

public void ConfigureServices(IServiceCollection services)
{
    // other services
    services.AddScoped<RoomsService>();
}

and then at the top of the razor file:

@inject RoomsService RoomService

and now your page methods can consume the service for data:

Task<List<Rooms>> GetMyRooms()
{
   return RoomService.GetAllRooms();
}

You could also turn the service methods into tasks and await them directly if you want to cut out some of the call chain for this, but that again is up to you.

There are a few things to keep in mind with this approach.

  • For the UI, I've had great results just in doing my best to get everything I can to run async, off the UI thread, which keeps everything very responsive. If you do the same, you may need to build up your app using CancellationTokens in strategic places so if you have a task that starts and then a user navigates away before it completes you can cancel the task on teardown. Look into component disposal and the like for this.
  • I left out the idea of JS interop and storing your data in your client browser session storage on purpose for this example. It is certainly something you can do, however wiring up any setting, retrieval, and synchronization logic won't be a small task as you have to start accounting for what exists in the browser DOM tree and when it is there, what will synchronize and what won't on its own, etc., so the complexity increases dramatically.
  • If your components inherit OwningComponentBase, scoped service lifetimes will be decreased to the life of the active component. This can be great in specific scenarios but if used overall it will make what I outlined above rather ineffective. Setup on OwningComponentBase is important if you use it, so look at the Further reading here.
  • You may wish to use different lifecycle methods depending on your needs, so you can render a loading page or component conditionally for example.Further reading here.
  • Lastly, if your list of rooms is slow to change and / or somewhat durable to stale data issues, you could make the case for building a long running server side cache that queries that list from persistence every so often and refreshes, and then in your RoomsService class you can look to that cache first before hitting your persistence layer on first load. This may be way overkill for what you have, but it could also reduce the wait time on that first load enough to make a difference.

This was quite a bit to throw at you, hope it helps!

Upvotes: 3

Related Questions