Reputation: 13
Why does it take ~15 seconds to display 50,000 records from a Web API in Blazor WASM, but <2 seconds in Blazor Server? Using virtualization in both cases. Is there something that should be made for the WASM case?
Code example:
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<Virtualize Items="@forecasts" Context="forecast" SpacerElement="tr">
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
</Virtualize>
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
// protected override async Task OnInitializedAsync()
// {
// // Simulate asynchronous loading to demonstrate streaming rendering
// await Task.Delay(500);
// var startDate = DateOnly.FromDateTime(DateTime.Now);
// var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
// forecasts = Enumerable.Range(1, 50000).Select(index => new WeatherForecast
// {
// Date = startDate.AddDays(index),
// TemperatureC = Random.Shared.Next(-20, 55),
// Summary = summaries[Random.Shared.Next(summaries.Length)]
// }).ToArray();
// }
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
As a side note: the records are displayed fast for both Blazor flavors if I use the commented lines of code in the @code{} instead of fetching data from the Web API.
Upvotes: 0
Views: 250
Reputation: 30167
Brian's answer tells you why and suggests a solution, This answer provides and overview of how to implement the solution.
You will find detailed notes and a Demo Project here https://github.com/ShaunCurtis/Blazor.ExploreRendering/blob/master/Documents/An-Interactive-Data-Pipeline.md. It's too big to provide as a SO answer!
This is a demo. In a production system you would implement a more detailed request object with paging and filtering and implement cancellation.
You need request and result objects:
namespace Blazor.Core.CQS;
public record ListRequest
{
public int StartIndex { get; init; }
public int PageSize { get; init; }
public ListRequest()
{
StartIndex = 0;
PageSize = 1000;
}
public static ListRequest Create(int startIndex, int pageSize)
=> new() { StartIndex = startIndex, PageSize= pageSize };
}
All requests return Result objects. This is the ListResult
.
namespace Blazor.Core.CQS;
public record ListResult<TItem>
{
public IEnumerable<TItem>? Items { get; init; }
public bool Successful { get; init; }
public string? Message { get; init; }
public int TotalCount { get; init; }
public ListResult()
{
Successful = false;
}
public static ListResult<TItem> Success(IEnumerable<TItem> items, int totalCount)
=> new() { Items = items, Successful = true, TotalCount = totalCount };
public static ListResult<TItem> Failure(string message)
=> new() { Message = message };
}
Requests are handled by Handlers, defined by the IListRequestHandler
interface.
namespace Blazor.Core.CQS;
public interface IListRequestHandler<TItem>
{
public ValueTask<ListResult<TItem>> Execute(ListRequest request);
}
The Server Version:
using Blazor.Core.Weather;
namespace Blazor.Infrastructure.Weather;
public class WeatherServerListRequestHandler : IListRequestHandler<WeatherForecast>
{
public async ValueTask<ListResult<WeatherForecast>> Execute(ListRequest request)
{
// Use a DbContext to get the paged data and totla count.
// Build a ListResult<WeatherForecast> and return it
}
}
The API Version
using Blazor.Core;
using Blazor.Core.Weather;
using System.Net.Http.Json;
namespace Blazor.Infrastructure.Weather;
public class WeatherAPIListRequestHandler : IListRequestHandler<WeatherForecast>
{
private readonly IHttpClientFactory _httpClientFactory;
public WeatherAPIListRequestHandler(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async ValueTask<ListResult<WeatherForecast>> Execute(ListRequest request)
{
var http = _httpClientFactory.CreateClient(AppConstants.WeatherHttpClient);
var httpResult = await http.PostAsJsonAsync<ListRequest>("/API/Weather/GetForecasts", request);
if (!httpResult.IsSuccessStatusCode)
return ListResult<WeatherForecast>.Failure($"The server returned a status code of : {httpResult.StatusCode}");
var listResult = await httpResult.Content.ReadFromJsonAsync<ListResult<WeatherForecast>>();
return listResult ?? ListResult<WeatherForecast>.Failure($"No data was returned");
}
}
Register different concrete implementations against the interface in the Server and Client projects.
Here's the Weather Page:
@using Blazor.Core.Weather
@using Blazor.Core.CQS;
@using Microsoft.AspNetCore.Components.Web.Virtualization
@inject IListRequestHandler<WeatherForecast> ListHandler
@inject IRenderStateService RenderState
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if(this.RenderState.IsPreRender)
{
<div>Loading...</div>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<Virtualize Context="forecast" ItemsProvider="this.GetRowsAsync">
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
</Virtualize>
</tbody>
</table>
}
@code {
private async ValueTask<ItemsProviderResult<WeatherForecast>> GetRowsAsync(
ItemsProviderRequest request)
{
var result = await ListHandler.Execute(ListRequest.Create(request.StartIndex, request.Count));
if (result.Successful && result.Items is not null)
return new ItemsProviderResult<WeatherForecast>(result.Items, result.TotalCount);
return new ItemsProviderResult<WeatherForecast>(Enumerable.Empty<WeatherForecast>(), 0);
}
}
Upvotes: 1
Reputation: 14573
Build your API to support paging. Use the Virtualize
components ItemsProvider
property not Items
. This will fix the issue. Implement the CancelationToken
for even more performance, as requests go out while the user is still scrolling.
This will reduce the number of elements the client is processing to the number of elements visible + (OverscanCount * 2) + 2.
An alternative to adding paging to your API is to use ODATA.
I have done demonstrations using 100k records with the paging request going all the way to the SQL server not being handled on the API server other than passing it through.
Odata's [EnableQuery]
and maintaining a IQueryable
(DbSet<>
implements IQueryable<>
) from your context makes this task trivial.
Upvotes: 2