aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Blazor Web App WASM download performance issues

Open wendellestradairely opened this issue 2 months ago • 47 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues

Describe the bug

There are too many wasm files being downloaded during startup and every time I reload the browser. It took almost 10 seconds to load all these files in local environment and several minutes to load these files into the browser when deployed to a remote/cloud server.

Image

This happens only in .NET 10 rc1 but not in previous versions (e.g. 9.0/8.0).

Expected Behavior

Must load faster like in .net 8/9

Steps To Reproduce

Compare two simple projects in both .NET 10 and .NET 9/8

  1. Create a new Blazor Web App.
  2. Use InteractiveAuto
  3. Global Interactivity location

Refresh/reload the browser. As you can see in .NET 10 it's download several wasm files and are very slow. This behavior doesn't happen in .NET 9 and .NET 8

Exceptions (if any)

No response

.NET Version

No response

Anything else?

No response

wendellestradairely avatar Oct 13 '25 06:10 wendellestradairely

@wendellestradairely thanks for contacting us.

During development asset caching is disabled to avoid serving stale assets. In production you should see the files served from cache. In .NET 8.0 and 9.0 we used a custom browser cache object to avoid the asset download altogether, but that came at a cost of size and complexity.

In .NET 10.0 an onwards we simply rely on the browser HTTP cache, which shouldn't show any difference in performance or behavior provided you are publishing in Release mode and running the published app.

During development we take some shortcuts to avoid potentially serving stale assets.

Please publish the app in Release mode, run the published output and let us know if you still have issues in that configuration.

javiercn avatar Oct 14 '25 09:10 javiercn

@javiercn It's already published in Release mode. I wish I could share the app link, but I could only show a video recording.

Here's the comparison between the same apps published in both .NET 10 and .NET 9. In .NET 10, it took over a minute for the page to become interactive whereas in .NET 9, it only took about 5 seconds for the page to become interactive. The issue happens on every normal page refresh.

(P.S. Don't mind the timer in the recording it's not accurate. Please see the browser's Network tab on the right.)

.NET 10 recording (trimmed)

https://github.com/user-attachments/assets/a3636745-df2c-40fb-a362-702db24723c9

.NET 9 recording

https://github.com/user-attachments/assets/805f2601-e69a-4a22-ac75-cb8aac6138ce

Same thing happens in Starter template (dotnet new blazor -int Auto -ai). How I published the app (tried 3 methods): dotnet publish dotnet publish -c Release Publish via Visual Studio Insiders (both in Azure App Service and On-prem IIS)

wendellestradairely avatar Oct 14 '25 12:10 wendellestradairely

@wendellestradairely what does the from column after time say?

javiercn avatar Oct 14 '25 13:10 javiercn

@javiercn You mean the "Fulfilled by" column? It says nothing. It's blank.

Image

Only the css and javascript are either memory or disk cached

Image

wendellestradairely avatar Oct 14 '25 13:10 wendellestradairely

Clear site data + Hard refresh = wasms are being downloaded with status 200 Normal refresh = wasms are always being downloaded with status 304

Image

wendellestradairely avatar Oct 14 '25 13:10 wendellestradairely

Here's an example using the starter template. Same issue.

Image

wendellestradairely avatar Oct 14 '25 13:10 wendellestradairely

UPDATE: I have upgraded to the latest .NET 10 RC.2 (10.0.0-rc.2.25502.107). I can no longer see a bunch of wasm files being downloaded on every browser refresh. It loads faster now. However, you can see a bunch of console errors.

Image

Another issue is that it doesn't transition to WebAssembly render mode even after several reloads:

Image

wendellestradairely avatar Oct 15 '25 11:10 wendellestradairely

@wendellestradairely

The errors you see in the console are caused by an unrelated problem with Wasm publish that was introduced in RC2 and reported elsewhere (#63970, #64041). This problem is fixed in the latest build.

Could you please try to reproduce your original download issue with the latest RC2 build (currently the same as the latest main)? You can get it from here: https://github.com/dotnet/dotnet/blob/main/docs/builds-table.md

oroztocil avatar Oct 16 '25 10:10 oroztocil

@wendellestradairely I apologize but a colleague just informed me that a final bit of the Wasm publish fix has not been released yet (pending this PR https://github.com/dotnet/dotnet/pull/2927). If you are willing to re-test the download issue later, I can ping you when a proper public build of the SDK is available.

oroztocil avatar Oct 16 '25 11:10 oroztocil

Not a problem @oroztocil. I can wait for the fixes. Thanks!

wendellestradairely avatar Oct 16 '25 13:10 wendellestradairely

@wendellestradairely Sorry for the delay. If you are still interested, can you reproduce your initial performance issue using the final GA release? https://dotnet.microsoft.com/en-us/download/dotnet/10.0

oroztocil avatar Nov 11 '25 15:11 oroztocil

@oroztocil I am currently in the final GA release and in publish -c Release mode. The error is now gone but the wasm files were not being cached. Is that an expected behavior?

Image

wendellestradairely avatar Nov 12 '25 05:11 wendellestradairely

Wasm files are downloaded repeatedly instead of being cached properly. Can this issue be fixed? This problem is preventing the upgrade of my Blazor Web App to upgrade to .Net 10.0

ManbirRakhra avatar Nov 12 '25 06:11 ManbirRakhra

I see the same issue in my release build using GA .NET 10. The files are not re-downloaded fully on return visits, but the interactive transition is still delayed until all assemblies are re-validated individually with 1 HTTP request for every assembly, even if it didn't change. It wouldn't be bad if this happened in parallel, but currently, I can see over 50 assemblies, each with a 50-millisecond round trip, firing one by one. That wastes a couple of seconds just to see a waterfall of sequential If-None-Match requests -> 304 Not Modified responses.

It may be caused by my failure to read all the breaking changes in ASP.NET 10, too. I just updated net9.0 to net10.0 in the csproj files, updated nugets, and changed dockerfile to reference dotnet/aspnet:10.0 and dotnet/sdk:10.0 . It's also possible that it was this bad in the previous version when the loader stashed the assemblies manually in the browser cache storage, and I didn't notice the wasted time. So, I'm not 100% positive that it's a regression. Maybe it's just an illusion that interactivity kicks in later now, but I'm pretty sure it used to work like in the .NET 9 video provided by wendellestradairely. I am using InteractiveAuto render mode for most of my project - the issue is not relevant to server-interactive render modes.

Still, it sounds like a lot of unnecessary HTTP trips just to say, "Yeah, the entire thing is unchanged." Assembly fingerprints should be immutable from the build time until the load time, so the loader should not even attempt to re-fetch assemblies if cached artifacts are still valid. Maybe some hot-reload thing is now flipped which delegates staleness check to server instead of doing it in the browser - not sure, just thinking out loud.

Next week, I may have time to debug it further and provide an isolated reproduction case using dotnet new blazor --interactivity Auto, in case it's not reproducible on your end.

balukin avatar Nov 13 '25 23:11 balukin

I'm using a blazor webassembly project written with .net 6 which worked fine, but when I just switched to .net 10 it gave me all the same problems, without any code changes.

suriya-kongsuk avatar Nov 14 '25 02:11 suriya-kongsuk

Same here, is there any fix for this? @oroztocil please help

jacekmichalski avatar Nov 14 '25 11:11 jacekmichalski

Hi folks.

We ran some tests on our end and we don't see a meaningful difference in results. To explain what is happening here, the browser chooses to send some HTTP requests to revalidate the .wasm files does so in the background. We believe the dev tools here are misleading because the content is served right away from the cache but it shows as 304 in the dev tools because the browser actually revalidates it. Now with that in mind, we want to ask you to run a somewhat simple test in your setups to see if you observe any difference. Note that you shouldn't be looking at the dev tools for this, but at some other metric like TTI or similar. You could use a performance counter in JS to measure the difference in startup time.

The following snippet will force the cache and it shows as such on the devtools. Please let us know if this makes any actual observable wall clock time difference in your setups.

    <script>
        (function() {
            const originalFetch = window.fetch;
            window.fetch = function(resource, options = {}) {
                // Check if the resource is a .wasm file
                const url = typeof resource === 'string' ? resource : resource.url;
                if (url && url.toLowerCase().endsWith('.wasm')) {
                    // Force cache mode for .wasm files
                    options = {
                        ...options,
                        cache: 'force-cache'
                    };
                }
                return originalFetch.call(this, resource, options);
            };
        })();
    </script>

javiercn avatar Nov 14 '25 19:11 javiercn

Hi, thank you for the response.

I can't check your suggested fetch patch to isolate it further now, but I just checked with Firefox and found the correct behavior there. All .wasm are resolved in ~0ms immediately from cache. In Edge and Chrome, the behavior is different - it triggers sequential revalidation of all WASM resources, blocking transition to interactive until all HTTP 304s finish sequentially. It is noticeably slower to transition into interactive, it's not a measurement error.

Tried with F12 tools closed/opened but this happens with both scenarios. In all of them I ensured to NOT have "Disable cache" checked but I will need to dig further to find out why and how because there are some tools installed on my OS (windows) in the proxy/http inspection area, so it's possible that something affects cache/no-cache decisions - not sure how, but there must be something browser-specific. This happens both for localhost and for external domains, though, so it's not localhost-specific. Browsers checked:

Chrome Version 142.0.7444.162 - issue exists Edge Version 142.0.3595.53 - issue exists Firefox 144.0.2 and 145.0 - no issue

If you have some public blazor test site that uses InteractiveAuto and .NET 10, I'll happily check if it's specific to my site, or my browser. If not, I'll try to set up public deployment+project that I can share next week, so we can isolate the underlying cause that affects this caching decisions.

Edit: Noticed one more thing - hitting F5 (not Ctrl+F5, without browser tools opened) always attaches Cache-Control: max-age=0 header in chromium, entering the site from a bookmark does not. This seems to work the same way for all sites - it just seems chrome/edge aggressively throws in this header on F5. I thought that it could be the explanation, but even when I enter from bookmark, it still reaches interactivity in ~2s on Firefox, compared to ~7s on Chrome (wall-clock time). There's something more that affects cache/revalidate decision.

balukin avatar Nov 14 '25 20:11 balukin

I encountered a similar problem. I suspect it is related to the network proxy set on my computer. I will continue to test.

bxjg1987 avatar Nov 15 '25 21:11 bxjg1987

+1 same issue here.

Brave and Chrome both attempts to revalidate all the .wasm files even though the files are served with cache control immutable.

I've upgraded from .NET 8 to .NET 10

I also ran a comparison of the .NET 8 Blazer Web App and .NET 10 Blazor Web App templates.

The .NET 10 template has the same issue.

schmidtgit avatar Nov 16 '25 10:11 schmidtgit

Hi. I have tried to create a new solution with net 9 and it work fine. but with net 10 (aslo new solution with the same config) browser did not cache anything and disk usage from Application storage empty too.

Visual studio 2026: Blazor web app: Framwork 9/10 Authenticate type None Interactive render mode Auto Interactive location Per page/component

I just realized this issue today when I demoed my app after upgrading to Net 10.

thnak avatar Nov 17 '25 14:11 thnak

@balukin Thanks for testing the issue.

Note that the difference in behavior between Firefox and Chrome (and other browsers) is due to the fact that Firefox actually supports the immutable cache-control flag, while Chrome does not. There is a long-standing open issue for adding support for the flag in Chromium.

oroztocil avatar Nov 18 '25 08:11 oroztocil

@oroztocil I've sent you an email to the live website which has the same problem.

jacekmichalski avatar Nov 18 '25 11:11 jacekmichalski

@jacekmichalski Thank you for the reproduction. I have seen your app and observe the sequential loading and the long time-to-interactivity as well. (Although your app seems to intermittently crash with error 500 which I assume might be due to free deployment configuration?)

I want to note that we are aware of the issue. I have previously reproduced it myself on a simple app with server-side HTTP logging deployed on Azure. There is an ongoing internal investigation in multiple directions, including assesing the extent of the problem, as well as providing short-term workarounds (see @javiercn's comment) and implementing a long-term fix.

oroztocil avatar Nov 18 '25 13:11 oroztocil

Hi folks.

We ran some tests on our end and we don't see a meaningful difference in results. To explain what is happening here, the browser chooses to send some HTTP requests to revalidate the .wasm files does so in the background. We believe the dev tools here are misleading because the content is served right away from the cache but it shows as 304 in the dev tools because the browser actually revalidates it. Now with that in mind, we want to ask you to run a somewhat simple test in your setups to see if you observe any difference. Note that you shouldn't be looking at the dev tools for this, but at some other metric like TTI or similar. You could use a performance counter in JS to measure the difference in startup time.

The following snippet will force the cache and it shows as such on the devtools. Please let us know if this makes any actual observable wall clock time difference in your setups.

<script>
    (function() {
        const originalFetch = window.fetch;
        window.fetch = function(resource, options = {}) {
            // Check if the resource is a .wasm file
            const url = typeof resource === 'string' ? resource : resource.url;
            if (url && url.toLowerCase().endsWith('.wasm')) {
                // Force cache mode for .wasm files
                options = {
                    ...options,
                    cache: 'force-cache'
                };
            }
            return originalFetch.call(this, resource, options);
        };
    })();
</script>

Implementing this workaround provides a noticeable performance improvement, particularly by decreasing load times during subsequent reloads. The snippet in App.razor enabled disk-cache retrieval once again. Nonetheless, resolving the root issue within the .NET repository remains essential.

ManbirRakhra avatar Nov 18 '25 19:11 ManbirRakhra

My project used to be. net9 blazer web app auto mode, and the auto mode worked well before.

After the official release of. net10, I upgraded. After I published the project to the server, through strict testing, I found that sometimes the wasm file returned 304, and after a period of time, it was accessed again. The response status was 200, and I had to wait more than 1 minute. Obviously, the auto mode failed.

I use the edge browser: version 142.0.3595.90 (official version) (64 bit), and the server release is released, windows server 2019, Run using kestrel.

bxjg1987 avatar Nov 20 '25 12:11 bxjg1987

Hi folks.

We ran some tests on our end and we don't see a meaningful difference in results. To explain what is happening here, the browser chooses to send some HTTP requests to revalidate the .wasm files does so in the background. We believe the dev tools here are misleading because the content is served right away from the cache but it shows as 304 in the dev tools because the browser actually revalidates it. Now with that in mind, we want to ask you to run a somewhat simple test in your setups to see if you observe any difference. Note that you shouldn't be looking at the dev tools for this, but at some other metric like TTI or similar. You could use a performance counter in JS to measure the difference in startup time.

The following snippet will force the cache and it shows as such on the devtools. Please let us know if this makes any actual observable wall clock time difference in your setups.

<script>
    (function() {
        const originalFetch = window.fetch;
        window.fetch = function(resource, options = {}) {
            // Check if the resource is a .wasm file
            const url = typeof resource === 'string' ? resource : resource.url;
            if (url && url.toLowerCase().endsWith('.wasm')) {
                // Force cache mode for .wasm files
                options = {
                    ...options,
                    cache: 'force-cache'
                };
            }
            return originalFetch.call(this, resource, options);
        };
    })();
</script>

I tried this method and it didn't always work.

bxjg1987 avatar Nov 25 '25 18:11 bxjg1987

Hi folks.

We ran some tests on our end and we don't see a meaningful difference in results. To explain what is happening here, the browser chooses to send some HTTP requests to revalidate the .wasm files does so in the background. We believe the dev tools here are misleading because the content is served right away from the cache but it shows as 304 in the dev tools because the browser actually revalidates it. Now with that in mind, we want to ask you to run a somewhat simple test in your setups to see if you observe any difference. Note that you shouldn't be looking at the dev tools for this, but at some other metric like TTI or similar. You could use a performance counter in JS to measure the difference in startup time.

The following snippet will force the cache and it shows as such on the devtools. Please let us know if this makes any actual observable wall clock time difference in your setups.

<script>
    (function() {
        const originalFetch = window.fetch;
        window.fetch = function(resource, options = {}) {
            // Check if the resource is a .wasm file
            const url = typeof resource === 'string' ? resource : resource.url;
            if (url && url.toLowerCase().endsWith('.wasm')) {
                // Force cache mode for .wasm files
                options = {
                    ...options,
                    cache: 'force-cache'
                };
            }
            return originalFetch.call(this, resource, options);
        };
    })();
</script>

I've noticed an issue with this script. After deployment of a new version of my blazor app I need to ask users to manually refresh browser cache to get latest improvements. It's painful to do this in a saas application with many users @maraf @oroztocil do you have any permanent fix?

jacekmichalski avatar Nov 26 '25 11:11 jacekmichalski

I have abandoned my Blazor project and switched to Angular due to the performance issues in Blazor .Net 10 including this issue in wasm caching.

wendellestradairely avatar Nov 26 '25 16:11 wendellestradairely

@javiercn I've been trying to find answers to this issue that appeared in .NET 10, and I saw a couple GitHub issues that were closed without resolution. With all due respect, if you're not seeing a difference in your tests, I think it's possible that you're either not replicating the issue, or the network conditions in the tests are "too optimal" to notice the symptoms.

I would love this to be something I'm doing wrong or overlooking, since that would be easy to fix. But it's looking like that's not the case.

Please see the below video. The left one is .NET 9. The right is .NET 10. Both deployed in Release configuration to Docker Hub. Both hosted on the same platform (Railway). Notice the difference in network calls. (Edit: Also, the .NET 10 one has the benefit of Cloudflare cache here. The .NET 9 app is being served straight from Railway. If I turn Cloudflare cache off for the .NET 10 one, I get 20-second load times like the one shown below.)

https://github.com/user-attachments/assets/ad0ac60f-e337-4df4-9667-468a69b70b7e

In both browsers, the WASM files have already been downloaded and cached.

The difference is that, in .NET 9, Blazor served them directly from cache storage without making any http requests. You don't see any calls in the network tab.

With .NET 10, an http request is sent for every wasm file to revalidate it. It returns 304 Not Modified and serves from cache after that, but all of the sequential http calls to check all the files is what's taking so long.

I'm seeing up to 20-second load times when Cloudflare's not in front of it. I know this phrase is over-used, but like, this is totally unusable. I can't ship this.

Image

Also, I have gigabit fiber, so I know it's not on my end.

Image

So, my questions regarding your testing are:

  1. Are you seeing the 304's for every WASM file, on every page refresh, even after everything's cached?
  2. How many WASM files are there with each page refresh?
  3. What's your average time for http requests that return the 304?

Because if you have, say, 100 WASM files and your latency is in the double or triple digits, you're going to notice the difference.

I can reduce the load time a bit with some extra Cloudflare cache rules. But it's still not practically usable. And there's no guarantee that the user will have a nearby CF edge node, or that they'll have low latency to it. Let's be honest, even people who love Blazor will admit that it's right on the edge of what's considered acceptable load times.

Like I said, I'd love to be wrong about this, and hopefully I'll soon feel very stupid about some dumb mistake I'm making. But I'm looking at my roadmap with dread, wondering if I need to throw everything out and rewrite my app.

bitbound avatar Nov 28 '25 05:11 bitbound