dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.34k stars 9.98k forks source link

[Blazor WebAssembly] Serious performance issues #21085

Closed julienGrd closed 4 years ago

julienGrd commented 4 years ago

Hello guys, I would like to discuss performance problems i met with my Blazor App.

It will be a very long text with a project example to illustrate this, so please read with attention before respond.

I rewrite since one year a silverlight app in blazor, and i notice since the begining some performance issues on some heavy views. The behavior is really bad, its a completely browser freezing and nothing respond (not only the app, all the browser, the loading effects in css are also freeze).

On the worst case of my app (navigate to a really big view to another), this freeze can take 12 seconds which is not just not acceptable.

I recheck the way of my components react before open this issue (be sure my components no render too many times) and on my side there is no problem, they are renders only one time even with 10 seconds freeze.

I precise i have this behavior only in webassembly, my app work also on server side and on this mode, the app is really more faster and i don't have this "freeze" effect (for example the 12seconds freeze in webassembly are transform in 1 or 2 second but this time make sense considering the amount of data shown, and the browser not freeze).

So i try to create a sample project to understand whats the problem : blazor ? the way i use it ? other ?

You will find the sample here https://github.com/julienGrd/BlazorTest This sample have 4 views, each view show the same data, 3 link showing a different table of 1000 lines, but by different ways. The question is not to know if 1000 line in a table is a good idea because obviously is not but observe the performance between the different ways

So the question is : why the first two view take 4 second for changing page and the others only 1 second ? i put some console.writeline in the OnAfterRender of my components to be sure everything is rendered one time.

we can also notice something when browser freeze in the browser console : some webassembly Garbage collector log like this

L: GC_MAJOR_SWEEP: major size: 9440K in use: 19819K
L: GC_MAJOR: (LOS overflow) time 31.00ms, stw 31.02ms los size: 14256K in use: 11098K
L: GC_MINOR: (Nursery full) time 26.06ms, stw 26.10ms promoted 952K major size: 15920K in use: 14653K los size: 23392K in use: 20592K

Im not able to understand the value provided by this log, webassembly is a black box for me. for example on my app when a 10 second freeze happen i have these values

L: GC_MAJOR_SWEEP: major size: 16688K in use: 34623K
L: GC_MAJOR: (LOS overflow) time 59.83ms, stw 59.98ms los size: 4240K in use: 2789K
L: GC_MINOR: (LOS overflow) time 0.81ms, stw 0.88ms promoted 0K major size: 16688K in use: 6798K los size: 4240K in use: 2789K

Thanks in advance for your help, for me it actually really a big deal because for now its a no go to put in production after one year of work.

I precise my app don't have table with 1000 rows but sometimes until 250, with 15 columns, and a content inside cell really complex, additionate with advance table scenario like grouping, filtering, resizing, etc. so my 10 seconds freeze are not really surprising if a 1000 row simple table put 4 seconds to show. It the actual behavior with the silverlight app and this last one can easily take 1000 rows like this without specific problems, so the company i work for will not accept answers like "there is too many data"

javiercn commented 4 years ago

@julienGrd thanks for contacting us.

We'll take a look at the issue in more depth and get back to you. One thing that comes to the top of my mind is that, (haven't looked at the app yet) is that it is likely a good idea for you to virtualize the list, meaning that you don't render the 1000 rows every time and that instead you only render the subset of rows that can confortable fit on the screen, and when the user scrolls in some way or performs a gesture with an arrow key, you load more data.

I can't tell for sure about the messages in the console since I'm not that familiar with the mono GC, but I would think that what you are observing are GC collections taking place on the Large Object heap (I guess mono calls it stack?) and that is going to cause some freezing.

I'm not sure if there is a way to configure the memory limits for the .NET Runtime, @SteveSandersonMS might now more about it, and that might palliate the problem, but it's only going to increase the upper bound.

The main issue to tackle there would be to understand why you are allocating so many large objects on the heap, which I think is causing the performance penalties. That also can explain why you are seeing a big difference between the client and the server. In the server case there is likely way more memory and also the server uses a difference GC and GC mode, which can explain why it's way faster, but ultimately, it comes down to the fact that there's always going to be a limit of rendered elements that is going to cause a slow-down in the app.

julienGrd commented 4 years ago

thanks for you reply @javiercn

Concerning the virtual scrollink i was thinking of that. However in my case the rows have dynamic height based on the content, wich make the thing really tricky to an efficient virtual scroll system (everything i see on internet was a fixed height row system). if you have some resources to achieve it i take it.

I can't tell for sure about the messages in the console since I'm not that familiar with the mono GC, but I would think that what you are observing are GC collections taking place on the Large Object heap (I guess mono calls it stack?) and that is going to cause some freezing.

Yeah it seem really related, each freeze superior at 2 second coincidate with this kind of logs messages

The main issue to tackle there would be to understand why you are allocating so many large objects on the heap, which I think is causing the performance penalties. That also can explain why you are seeing a big difference between the client and the server. In the server case there is likely way more memory and also the server uses a difference GC and GC mode, which can explain why it's way faster, but ultimately, it comes down to the fact that there's always going to be a limit of rendered elements that is going to cause a slow-down in the app.

Yes sure, for now i don't have the hand on the way the app present data, i just make the technical migration of an app where sometimes the table have 250 rows, i know its not recommanded, but the actual app work like that since years without problem.

Thats why i think you have to focus on my implementation 1 (Grid V1) and 3 (Simple Table) : both are made with blazor, both just render a table of 1000 rows. the first take 4 second, the other take 1 second. I have to understand why because if i can divide the execution time by 4 in my app, i win, the worst scenario will be 3 second (which can be acceptable considering the amount of data) and 98% of my app will be really fast.
But there is something in my implementation which make this grid 4x slower. Im sure its related the way the components are imbricated and communicate each others, but i make things like this to handle specific scenario and don't know if there is "better way".

So the goal is to find an implementation of my first component with keeping behavior of selection and Sorting but with the performance of implementation 3.

and it's not so easy.

mlathan commented 4 years ago

@julienGrd I can confirm your observations. Currently I'm also experimenting with a TableView which displays fixed height rows and variable widths columns. For this I have a container component, the TableView, which inside renders the grid cells in two for loops (rows, cols). Like @javiercn mentioned I already virtualized the scrolling, react on resizing and also use HTML Grid Layout in a clever way to minimize required HTML for table layout. I can have 1.000.000 rows with constant performance.

I tried different approaches for the cell rendering inside the TableView div (visible 40 x 13 = 520 cells):

  1. RenderFragment with parameter (the data to show in the cell) as Delegate, which contains a Component. This is by far the slowest. Parameter binding in Blazor is very expensive because internally it is based on Type and String matching on properties. Component instancing also.

  2. RenderFragment with parameter (the data to show in the cell) as Delegate, which contains HTML. Second slowest. My observation is, that it is expensive to transport the Parameter to the RenderFragment. Bit faster because no Component involved. But HTML wich is inspected by Blazor.

  3. Directly render HTML in the for loops of the TableView. Third slowest. There is again overhead in that Blazor "looks at the" HTML elements inside the Razor environment. By "looks at" I mean it always flows through the RenderTreeBuilder which does caching and re-using of DOM elements and stuff.

  4. Use StringBuilder to generate the whole set of grid HTML in a dedicated render method inside TabelView and hand this as MarkupString to the Razor code. Idea is to prevent Blazor from "looking" at the HTML elements. Fourth slowest ... and fastest approach. BUT you loose all the blazor comfort ... events @key etc.! With this approach I can have 50 fps on my machine while scrolling. The more characters you generate inside the StringBuilder the slower it gets.

... and yes I also got a lot of GC messages on the log. In a another issue, can't remeber which, a person asked whether the GC log messages are problematic. They are not. BUT like @javiercn said is an indication that we use Blazor mechanic in a way that pushes the WASM Blazor mechnic to its limits.

For this kind of stuff we definitely need AOT compilation for faster code execution. The current C# .Net Blazor WASM mechanic isn't able to re-render, with parameter setting, lots of Components many times a second.

julienGrd commented 4 years ago

@mlathan Is your virtualization system is something we can see somewhere (i speak about the source code) ? (i see you have fixed height row, which is not my case, but it can can be a good start).

I put this subject aside, I don't know how to make an efficient virtualization system with my dynamic height rows, and i don't understand how we can reach the limit of the system with a simple 250 rows table. The webassembly version is really slow, i hope AOT compilation will be a game-changing. I really hope don't have to manage all my grid in javascript, it will be a real disappointment for this technologie (which is amazing, for now)

pranavkm commented 4 years ago

I've written a draft for authoring performant Blazor applications as part of https://github.com/dotnet/AspNetCore.Docs/issues/18277#issuecomment-627606580. It includes a sample for virtualization. Could you give that a try?

In addition, we're looking at continuing to invest in areas of 5.0 including,

julienGrd commented 4 years ago

@pranavkm thanks for this code, its really a cool code I would not have done better.

I integrate in my test app : https://github.com/julienGrd/BlazorTest

You now have a new menu Fetch Data Virtualize where i use your virtualization system in my grid, the performance are really good, the ordering feature works well, selection too included keyboard navigation (except some scroll synchronisation with my selected items, i have to work on that).

Now i have to make it work with different height rows...

I will add more and more feature on the future, to see if we keep good performance

SteveSandersonMS commented 4 years ago

@julienGrd In case it's of interest to you, I've also proposed what I think is a better way of doing list virtualization that should perform better still and may avoid some of the scroll synchronization issues you've mentioned. Might be worth trying it out in your app: https://github.com/pranavkm/repro/pull/2

julienGrd commented 4 years ago

@SteveSandersonMS I integrate your virtualized system on my grid https://github.com/julienGrd/BlazorTest, on the menu Fetch Data -Virtualize 2

This new system is really cool, we don't have the "blank" effect when we scroll, it's really smooth !

However there is some effects a little bit strange, i don't take time to debug the code but maybe you have an idea :

thanks !

eanpr3 commented 4 years ago

Hi, I also facing with this problem browser freeze while UI rendering. I hope they have solution for this problem asap.

Thanks,

JinShil commented 4 years ago

I've converted @julienGrd's BlazorTest application to Blazor-Server for comparison. You can find it at https://github.com/JinShil/BlazorTest

You'll notice that the Blazor-Server version is quite a bit faster than the Blazor-WASM version when selecting between pages, though there is still a little latency.

The conversation here in this issue seems to be centered around how to virtualize the grid. The "Fetch data Simple Table" version and the "Fetch data JS" version are both very fast when selecting between pages, so I'm wondering why virtualization is even necessary. I would expect a WASM application to be similar in performance to the "Fetch data JS" version.

julienGrd commented 4 years ago

@JinShil yes its also my conclusion on my app, server-side is really faster than client side in all aspects.

On my side too, even if virtualization will solve some problems, i have some heavy view in my app where virtualization is not an option, and they are actually too slow to be usable.

i hope AOT with .net 5 will be a game changer !

legistek commented 4 years ago

I don't want to suggest anyone close this issue, but it's almost the same as this: https://github.com/dotnet/aspnetcore/issues/5617

Overly aggressive GC, which also appears to block the WASM thread (please correct me if I'm wrong), is a huge problem right now. I'm not sure that AOT would solve this. GC really has to happen in a background thread I think. I believe WASM threads are now enabled by default in Chrome so I would hope the Mono team would be on top of that.

Another huge issue from what I've found, consistent with what others have observed, is that load time is directly proportional to DOM size and complexity. No matter what I do to optimize (or intentionally de-optimize) my C# code, performance is roughly the same. This screams to me of WASM-JS interop being the bottleneck. That's also not going to be solved with AOT.

So I agree with @SteveSandersonMS and others about the benefits of virtualization. In fact I think it's going to be mandatory to get any kind of reasonable performance out of Blazor WASM. It's a shame no one has figured out how to make it work without constant row heights. (VirtualizingStackPanel.cs in WPF is a whopping 13,000 lines of code, so this clearly is not easy).

That said, even with virtualization I find thread-blocking GC events during scrolling. It's usable, but not good. Again this is directly proportional to the size and complexity of the DOM being generated during the scroll (e.g., the rows).

This finding from @mlathan is curious:

Use StringBuilder to generate the whole set of grid HTML in a dedicated render method inside TabelView and hand this as MarkupString to the Razor code. Idea is to prevent Blazor from "looking" at the HTML elements. Fourth slowest ... and fastest approach. BUT you loose all the blazor comfort ... events @key etc.! With this approach I can have 50 fps on my machine while scrolling. The more characters you generate inside the StringBuilder the slower it gets.

It's not clear to me why StringBuilding would be a noticeable bottleneck. Is it possible for you to time the actual string building inside your method and see if it's a significant fraction of the total time it takes your component to be visible on the screen? If the string building itself takes negligible time - which it should even in interpreted mode - then the bottleneck is in the communication of that string to the JS runtime (remember WASM can't touch the DOM so it all has to go to JS). I don't think AOT would solve that.

ghost commented 4 years ago

Any news on the aot performance? How fast the aot performs vs the current wasm? The template wasm loads 1-1.5 secs in my laptop the 2nd time around (some parts cached). and loads 4 secs in my phone the 2nd time around (some parts cached).

legistek commented 4 years ago

ny news on the aot performance? How fast the aot performs vs the current wasm? The template wasm loads 1-1.5 secs in my laptop the 2nd time around (some parts cached). and loads 4 secs in my phone the 2nd time around (some parts cached).

There's not going to be any way to know for sure until the Blazor team makes it available for us to test, but when playing with Uno, I found a noticeable but not overwhelming improvement. (Sorry to be so imprecise there. I didn't time anything. At the end of the day all that matters is how it feels to users). It'll probably be acceptable for most applications - provided they employ a UI framework like MD that runs entirely on the JS side, and likewise implement complex custom components on the HTML/JS side. Even then, tricks like virtualization are going to remain essential.

Unfortunately even with AOT I think a 100% Blazor/C#-based UI framework, which is really what I would like to see, is probably out of reach for the time being. DOM building just isn't what WASM was meant for. Would that the creators in their infinite wisdom had sought fit to give WASM modules direct DOM access. Alas.

limefrogyank commented 4 years ago

I find that virtualized list performance is proportional to the complexity of the item renderfragment that's being virtualized. The first list has items that are only this:

<div style="display:flex; flex-direction:row;" class=@($"ms-List-cell-default{(selection.SelectedItems.Contains(context.Item)?" is-selected":"")}") data-is-focusable="true" @onclick=@(()=> { selectionZone.HandleClick(context.Item, context.Index); DebugText = context.Item.Key + " clicked";})>
    <img height="25" width="25" src=@context.Item.ImgUrl />
    <em>This is item #@context.Item.Key.</em>
    <span style="margin-left:10px;">@context.Item.DisplayName</span>
</div>

The second list has items that are composed of many components, one of which is making a jsinterop call, but even when I remove it, performance isn't great. I'm hoping AOT will speed this up.

Video of simple list: Virtualized List (Simple)

Video of list with items made of many components: Virtualized DetailsList (Complicated)

legistek commented 4 years ago

@limefrogyank when you run the tests and check your browser console do you notice a lot of GC events in the second vs. the first?

ghost commented 4 years ago

ven with AOT I think a 100

I'm actually happy once the blazor wasm has loaded. I dont see any lags for the default blazorwasm template and my code on top of it. But the initial loading is quite slow, even if the page was loaded 2nd time around (some parts cached). and wont reach google.com like performance for simple minimalist websites with hello world text only. So I am waiting to see if aot can reduce the initial loading time to google.com like even if it is just cached.

limefrogyank commented 4 years ago

@limefrogyank when you run the tests and check your browser console do you notice a lot of GC events in the second vs. the first?

Yes, there are a few. Not every lifecycle event, but 3 or 4 by the time I get down to the bottom of the list.

Liander commented 4 years ago

@julienGrd Thanks for putting this together! I have some observations I like to share.

In FetchDataComponent.razor:

    @if (CurrentPage == Page.Page1)
    {
        <ListForecastV1 Items="forecasts1"/>
    }
    else if (CurrentPage == Page.Page2)
    {
        <ListForecastV1 ... />
        ...
    }

It is easy to get the impression that the three declarations will give you three instances of the list/grid that you select between. But that is not what will happen. The render-tree will be changed for each selection, giving a new grid and with the old one deleted, so 1000 rows created/disposed for each selection.

Try using a single instance instead:

    <ListForecastV1 Items="GetCurrentPageItems()" /> 

This will reuse the grid instance and all the row instances when rendering a new page which will be a lot easier on the GC. I tried it on the V1-version with better speed, but the virtualization-versions might assume new instances since I saw grid-row components where pinned to Elements with @key, so beware...

It would be good to have three instances to select between and have pinned rows and no creation/deletion when selecting page. But I am not aware of a way to do that.

julienGrd commented 4 years ago

@julienGrd Thanks for putting this together! I have some observations I like to share.

In FetchDataComponent.razor:

    @if (CurrentPage == Page.Page1)
    {
        <ListForecastV1 Items="forecasts1"/>
    }
    else if (CurrentPage == Page.Page2)
    {
        <ListForecastV1 ... />
        ...
    }

It is easy to get the impression that the three declarations will give you three instances of the list/grid that you select between. But that is not what will happen. The render-tree will be changed for each selection, giving a new grid and with the old one deleted, so 1000 rows created/disposed for each selection.

Try using a single instance instead:

    <ListForecastV1 Items="GetCurrentPageItems()" /> 

This will reuse the grid instance and all the row instances when rendering a new page which will be a lot easier on the GC. I tried it on the V1-version with better speed, but the virtualization-versions might assume new instances since I saw grid-row components where pinned to Elements with @key, so beware...

It would be good to have three instances to select between and have pinned rows and no creation/deletion when selecting page. But I am not aware of a way to do that.

thanks for the tip ;-)

AlfonChitoSalano commented 4 years ago

i could confirm this slow performance if I use for loop to render html for 200 or more data at > 20 secs. whilst the old school mvc could render just 1-3 secs.

xrkolovos commented 4 years ago

We have started using blazor in our projects and we are very afraid of how the performance of this apps will be. We use good machines for development, and we still see performance issues when using our apps. Navigation is slow, and at some random times the apps gets really slow. Refresh solves temporary the problem, but after some use it gets having bad response times. Our projects use telerik controls.

Its easy to understand this performance issue seeing the demos all the companies having blazor controls vs angular ones.

telerik blazor grid vs telerik angular grid syncfusion blazor grid vs syncfusion angular grid syncfusion navigation in demos is also very slow devexpress blazor grid vs devexpress asp.net core grid (js based) radzen grid

Playing around with all this demos vs angular or js demos we can see serious response times in clicks and html renders. So, can we expect blazor apps to be smooth when growing in regular size apps? Is this js - webassbly interop is always going to be behind js ui frameworks? Our angular apps have very good response times. Is there a blazor demo by dotnet team, that there are rendered a fair big amount of components?

Also, is there any plans having expected specs for pcs, so they can run blazor apps smoothly?

The blazor framework in general is very nice, and the all concept around this, as long it works nice. We are afraid of an other silverlight-like bad call.

SteveSandersonMS commented 4 years ago

@xrkolovos We're working hard on improving perf, especially for intensive component-rendering scenarios like large grids, in 5.0. This work is ongoing in #22432. You'll have to wait for a couple more preview releases before we get in the optimizations we have planned and then you can measure how much difference it makes for your scenarios.

In particular one of our goals is to ensure that a large grid (20x200 in our benchmark cases) will render sufficiently fast that humans don't perceive delay. When combined with UI virtualization, the grid could contain arbitrarily many rows without extra processing cost. So hopefully you'll be able to feel confident building much more complex UIs in 5.0 than feels possible in 3.2, but again I'm afraid you'll need to wait a couple more preview releases to confirm this yourself in your own scenarios.

andersme commented 4 years ago

@SteveSandersonMS It's really great to hear that you guys are working hard on this. We're starting to use Blazor at work for new projects and are willing to deal with growing pains and grow with the technology. I think most .NET developers will be easily persuaded to use Blazor because of it's familiarity, but mass adoption will depend almost entirely on performance. Moving forward, I would love to see performance benchmarks for Blazor against other UI frameworks be a little more visible, so we can see the progress over time. Also, do you have an approximation of when we will see a preview release containing these performance improvements? Again, love what you guys are doing and keep up the great work.

allan-mobley-jr commented 4 years ago

@SteveSandersonMS I noticed this "UI Freeze" when making an API call from Blazor WASM.

allan-mobley-jr commented 4 years ago

@SteveSandersonMS Sorry my fat fingers misfired and hit submit above.

At first I thought the freeze was a result of the loop rendering the HTML table and data. It was a lot to render (four thousand items), and I was testing this out to get a better feel.

Then I removed the rendering part and just did the call to see if the "UI Freeze" went away, and to my surprise it persisted until the call was complete.

The call is asynchronous to an Azure Function, which talks to a Cosmos backend.

Finally, I timed the call and then the rendering and found that the rendering was blazing fast...but the API call took 3 secs minimum.

This same call directly from JavaScript or postman took a couple hundred milliseconds.

xrkolovos commented 4 years ago

@andersme i opened #23571

datvunguyen commented 4 years ago

Just want to chime in and say that the whole UI becomes sluggish when there is a huge amount of render (let's say 5000 rows of data). For instance, checking a checkbox (with a simple bind) within the same component takes about a full second for the event to become apparent (the check mark appears) on my really fast machine, even when the checkbox doesn't change/filter any of the data that was rendered besides the property that was bound to it. I could understand a freeze happening if checking the checkbox is filtering data, but toggling a single bool should be instantaneous even when the DOM is gigantic. This performance issue does not happen with JS frameworks like vue. I happen to know because I am working on a POC to show my boss we can port an app to Blazor that was previously rendered with vue.

This should be looked at.

Edit: FYI I recreated my app as a Blazor Server app and it does not suffer the same issue.

SteveSandersonMS commented 4 years ago

In .NET 5, we put a lot of focus into improving the performance of Blazor WebAssembly. This involved many investigations and resulted in changes to Blazor's rendering pipeline, the underlying .NET class libraries, and the underlying .NET runtime. The net effect of all this is:

Altogether, we hope this will make a very significant difference to the size and complexity of UIs you can render on Blazor WebAssembly while maintaining excellent speeds. Developers do still need to think carefully about performance, but hopefully the new features and guidance will give you the ability to get the results you want. We are still aiming to implement AoT compilation in .NET 6 but hopefully the improvements in .NET 5 will cover the requirements for most sophisticated UI scenarios well now :)

Since .NET 5 completely changes the perf landscape, I'm now going to close this issue. Anyone who finds surprising perf issues in the future, and is working on .NET 5 and has already read and followed the guidance linked above, could you please post a new issue describing your scenario? Thanks!

breadnone commented 3 years ago

Our project suffers from this and I don't really get why this was closed!.. The given solution in the mft doc is more like to avoid the problems and changing the design of our project will cost us a lot... I've lost my words, honestly

allan-mobley-jr commented 3 years ago

I, on the other hand, am seeing remarkable improvements in Blazor WASM .Net 5 from where we were with Blazor .Net Core 3.

Sent from my T-Mobile 5G Device Get Outlook for Androidhttps://aka.ms/ghei36


From: Cattywampus notifications@github.com Sent: Saturday, November 7, 2020 10:23:53 AM To: dotnet/aspnetcore aspnetcore@noreply.github.com Cc: Allan Mobley allan.mobley.jr@qbcart.net; Comment comment@noreply.github.com Subject: Re: [dotnet/aspnetcore] [Blazor WebAssembly] Serious performance issues (#21085)

Our project suffers from this and I don't really get why this was closed!.. The given solution in the mft doc is more like to avoid the problems and changing the design of our project will cost us a lot... I've lost my words, honestly

— You are receiving this because you commented. Reply to this email directly, view it on GitHubhttps://github.com/dotnet/aspnetcore/issues/21085#issuecomment-723458884, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AJFK627IGUMPGVMQYQ4DJZDSOVRATANCNFSM4MOAFWBA.

isc30 commented 3 years ago

I've found that when the components are built around CSS variables passed down, rendering in WASM can get really slow