Aspnetcore: [Blazor] Parallel API calls seem to run sequentially in Chrome

Created on 11 Oct 2020  路  10Comments  路  Source: dotnet/aspnetcore

Describe the bug

Parallel API calls runs sequentially in Google Chrome in WebAssembly if DevTools are closed.

To Reproduce

Run multiple parallel tasks in WebAssembly calling, for instance, an API.
Introduce artificial delay on API side to ensure requests are running in parallel.
Combine calls into list of tasks and do await Task.WhenAll(listOfRequests).
Measure total execution time and see it equals to ServerSideDelay*NumberOfRequest in Chrome.

Run the same code with Developer Tools Opened in Chrome - works significantly faster, and it is visible in the network tab that requests are running in parallel.
Run the same code in Safari - works significantly faster.

Further technical details

  • ASP.NET Core 3.1
    .NET Core SDK (reflecting any global.json):
    Version: 3.1.302
    Commit: 41faccf259

Runtime Environment:
OS Name: Mac OS X
OS Version: 10.15
OS Platform: Darwin
RID: osx.10.15-x64
Base Path: /usr/local/share/dotnet/sdk/3.1.302/

Host (useful for support):
Version: 3.1.6
Commit: 3acd9b0cd1

.NET Core SDKs installed:
2.2.207 [/usr/local/share/dotnet/sdk]
3.1.101 [/usr/local/share/dotnet/sdk]
3.1.102 [/usr/local/share/dotnet/sdk]
3.1.300 [/usr/local/share/dotnet/sdk]
3.1.302 [/usr/local/share/dotnet/sdk]

.NET Core runtimes installed:
Microsoft.AspNetCore.All 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.2 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.4 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.6 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.1 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.2 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.4 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.6 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

  • Rider 2020.2.4
  • Chrome 85.0.4183.121
  • Safari 15610.1.28.1.9
affected-very-few area-blazor blazor-wasm bug investigate severity-minor

Most helpful comment

This isn't a problem with task parallelism in Blazor, it is a limitation in Chrome.

Chrome will only let a single request exist for the same URL at a time, so if you make 50 requests for /hello it will only make one request at a time. If however you request /hello?id={x} then it will request them all immediately.

You can prove this restriction also exists in JavaScript like so...

  1. Add the following to your Counter.razor file, and add a button that executes it.
    private async Task DoJS()
    {
        queryTime = "calculating";

        var sw = Stopwatch.StartNew();
        await JSRuntime.InvokeVoidAsync("doRequests", 30);
        sw.Stop();

        queryTime = sw.ElapsedMilliseconds.ToString();
    }

  1. Add this script file to your blazor project and reference it from index.html
window.doRequests = function (count) {
    var promises = [];
    for (var i = 0; i < count; i++) {
        let current = i;
        let promise = fetch("https://localhost:6001/hello").then(response => {
                return response.json();
            }).then(hello => {
                console.log("Finished " + current + " " + hello)
            });
        promises.push(promise);
    }
    return Promise.all(promises);
};

This is because Chrome will lock the cache item until the request is complete. If you want to allow concurrent requests to the same URL then you need to ensure no-store is specified for caching.

I proved this in JS by changing the fetch line as follows

        let promise = fetch("https://localhost:6001/hello", { "cache": "no-store" }).then(response => {

I've not looked at how to do the same with HttpClient.

All 10 comments

Could you post repro code? I suspect the issue might be in your code

    private async Task IncrementCount()
    {
        currentCount++;
        var sw = Stopwatch.StartNew();
        var tasks = Enumerable.Range(0, 10).Select(x => Http.GetFromJsonAsync<HelloResponse>("hello")).ToList();
        await Task.WhenAll(tasks);
        sw.Stop();
        queryTime = sw.ElapsedMilliseconds.ToString();
    }

Thanks for contacting us.
We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

This isn't a problem with task parallelism in Blazor, it is a limitation in Chrome.

Chrome will only let a single request exist for the same URL at a time, so if you make 50 requests for /hello it will only make one request at a time. If however you request /hello?id={x} then it will request them all immediately.

You can prove this restriction also exists in JavaScript like so...

  1. Add the following to your Counter.razor file, and add a button that executes it.
    private async Task DoJS()
    {
        queryTime = "calculating";

        var sw = Stopwatch.StartNew();
        await JSRuntime.InvokeVoidAsync("doRequests", 30);
        sw.Stop();

        queryTime = sw.ElapsedMilliseconds.ToString();
    }

  1. Add this script file to your blazor project and reference it from index.html
window.doRequests = function (count) {
    var promises = [];
    for (var i = 0; i < count; i++) {
        let current = i;
        let promise = fetch("https://localhost:6001/hello").then(response => {
                return response.json();
            }).then(hello => {
                console.log("Finished " + current + " " + hello)
            });
        promises.push(promise);
    }
    return Promise.all(promises);
};

This is because Chrome will lock the cache item until the request is complete. If you want to allow concurrent requests to the same URL then you need to ensure no-store is specified for caching.

I proved this in JS by changing the fetch line as follows

        let promise = fetch("https://localhost:6001/hello", { "cache": "no-store" }).then(response => {

I've not looked at how to do the same with HttpClient.

Thank you. It is helpful.

@vyacheslav-mikhaylov You are welcome. Should this be closed now as it is not Blazor related?

This is how we can use it with HttpClient

            var cacheControl = new CacheControlHeaderValue()
            {
                NoCache = true
            };

            builder.Services.AddScoped(
                sp => new HttpClient
                {
                    BaseAddress = new Uri("https://localhost:6001"),
                    DefaultRequestHeaders =
                    {
                        CacheControl = cacheControl
                    }
                });

And to enable CacheControl header for CORS of the API side:

     app.UseCors(
         policy =>  
            policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
                      .AllowAnyMethod()
                      .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, HeaderNames.CacheControl)
                      .AllowCredentials());

@vyacheslav-mikhaylov You are welcome. Should this be closed now as it is not Blazor related?

Sure, let's close it. Thanks again.

Thanks for posting the C# solution. Perhaps you could post this as a question on StackOverflow with an answer too? I'm sure others will spot this at some point.

Was this page helpful?
0 / 5 - 0 ratings