Aspnetcore: Blazor performance optimizations

Created on 1 Jun 2020  路  12Comments  路  Source: dotnet/aspnetcore

Battle plan:

  • [x] Quantify 3.2.0 perf

    • ... for selected scenarios:



      • [x] 3rd-party unoptimized code (ComplexTable)


      • [x] 3rd-party optimized code (PlainTable)


      • [x] 1st-party optimized code (FastGrid)



    • [x] ... providing useful breakdowns of:



      • [x] ... time in .NET rendering logic (BuildRenderTree calls, diffing, etc.)


      • [x] ... time in the JS DOM-updating logic



  • [x] Identify and implement optimizations, in each case quantifying the change:

    • [x] Inside Blazor code



      • [x] ... inside the Razor compiler, to reduce the amount of stuff we render at runtime


      • [x] ... inside the .NET rendering logic


      • [x] ... inside the JS DOM-updating logic



    • [x] Inside Mono interpreter (have provided scenarios to Vlad for this effort)

    • [ ] In browser's WASM engine (this is something Vlad/Zoltan are leading and might not complete before 5.0 ships) Out of scope for this issue.

    • [x] In 3rd-party code



      • [ ] ... by providing more guidance/docs Covered below


      • [ ] ... by providing performant base classes (virtualization, base grid) Covered in #24179



    • [x] As a result of switching to CoreFX for 5.0

  • [ ] Consider backporting optimizations to 3.2.x if they yield big benefits without involving changes to user code

Update: Results of investigation

After a very extensive investigation, here are things I think we should do to complete this for 5.0:

  • [ ] Write up the main findings from the investigation

    • [ ] Exactly how perf differs between 3.2.0 and 5.0 Preview 8

    • [ ] Why the perf numbers for FastGrid/PlainTable/ComplexTable are what they are. That is, provide breakdowns showing how use of each feature adds to the totals, explaining the differences in perf in ways that justify the perf advice we will give.

  • [x] Ensure we can keep collecting useful profiling data in the future (not just on my machine)

    • [x] Add the ConsoleRunner project to the repo so we can easily run Blazor scenarios on the desktop interpreter

    • [x] Begin a discussion with @BrzVlad and others responsible for the .NET interpreter about how we could merge in something like the trace collection logic so others can get profiling data in the future without needing a custom build of the runtime. Filed https://github.com/dotnet/runtime/issues/40617

  • [x] Implement optimizations inside Blazor

    • [x] RenderTreeArrayBuilder optimization - this makes the core rendertreebuilder logic faster by reducing the amount of indirection and struct copying (including by making RenderTreeFrame mutable). This gives about a 10% gain on the FastGrid benchmark. https://github.com/dotnet/aspnetcore/issues/24464

    • [x] In RenderTreeDiffBuilder.InitializeNewAttributeFrame, use culture-insensitive ordinal string comparison when looking for attribute names starting with on. This is ultra-trivial and gives a 2-3% gain on the PlainTable/ComplexTable benchmark. https://github.com/dotnet/aspnetcore/issues/24465

    • [x] Optimize parameter writer dictionary lookup, for example using an object-identity dictionary instead of one that hashes string keys. This gives nearly 10% gain for the ComplexTable benchmark. https://github.com/dotnet/aspnetcore/issues/24466

    • [x] Optimize multiple-attributes overwrite detection by eliminating the string-hashing dictionary. For example, the SimpleStringIntDict prototype gives a 17% boost to the ComplexTable benchmark if it's changed to pass 3 catch-all params to each cell component. https://github.com/dotnet/aspnetcore/issues/24467

  • [ ] Write up docs on how to write better-performing Blazor WebAssembly apps. For example:

    • How much overhead you should expect from each extra layer of components you add, and each extra parameter you pass. Hence you should consider inlining child components if you're rendering a large number of them.

    • Why it's so important to use IsFixed when cascading values to large numbers of receivers.

    • That @attributes is relatively expensive

    • Why, for perf-criticial components, you should consider implementing your own manual parameter assignment logic on SetParameterAsync, and how to do it efficiently.

    • How and why to use <Virtualize>

The specific optimizations I've listed above aren't the only ones I'm aware of. I've tried many things based on the profiling data. These optimizations are the ones that have the biggest impact for the least cost.

Done area-blazor blazor-wasm enhancement

Most helpful comment

Would it be possible to create "native" WebAssembly benchmarks to test Blazor against? Just to be able to decide whether any given performance related issues are present because of WebAssembly (/JS interop DOM manipulation) or because of the Blazor runtime?

All 12 comments

Would it be possible to create "native" WebAssembly benchmarks to test Blazor against? Just to be able to decide whether any given performance related issues are present because of WebAssembly (/JS interop DOM manipulation) or because of the Blazor runtime?

The performance is a huge issue. I was expecting "close to native" speeds from .net wasm but in my experience it's far from it. I'm working on an application that heavily relies on cryptography, and the performance different from .net core console and blazor wasm is staggering.

For example, one of the nuget packages I use is SRPDotNet. I install the nuget package, copy paste the code from their example, and run it on each platform. The time to run the sample on each is

.NET Core Console
0.0784997 seconds

Blazor WebAssembly 3.2.0
10.45238 seconds

Blazor is over 133x slower! Unfortunately for me, this is the user sign in process for my application, and that length of time to sign in is unacceptable. This was also tested on my powerful desktop PC, I imagine it would be much worse on a mobile device.

I also considered that the specific nuget package I was using wasn't very optimized and tried another implementation with the same poor results in blazor.

If someone on the blazor team wants to try this out for themselves install the SRPDotNet nuget pacakge and run this code.

```C#
Console.WriteLine("Starting SRP");
var sw = Stopwatch.StartNew();
var username = "johndoe";
var password = "password";
var hash = SHA256.Create();
var parameter = new Bit2048();
var srp = new SecureRemoteProtocol(hash, parameter);
var privateKey = SecureRemoteProtocol.GetRandomNumber().ToBytes();
var serverKey = SecureRemoteProtocol.GetRandomNumber().ToBytes();

        var verificationKey1 = srp.CreateVerificationKey(username, password);

        var user1 = new SRPUser(username, password, hash, parameter);
        var a = user1.GetEphemeralSecret();
        var authentication1 = user1.StartAuthentication();

        var svr1 = new SRPVerifier(hash, parameter, verificationKey1, authentication1.PublicKey);

        var b = svr1.GetEphemeralSecret();
        var challenge1 = svr1.GetChallenge();
        var session1 = user1.ProcessChallenge(challenge1);

        var hamk = svr1.VerifiySession(session1);
        sw.Stop();
        Console.WriteLine($"SRP took {sw.Elapsed.TotalSeconds} seconds");

```

@shawnshaddock Blazor WebAssembly runs using an IL interpreter. For the 3.2 release, it's about 10-15x slower than .NET Core for the typical UI workloads that we expect Blazor users to be writing. We expect improvements in this number as part of .NET 5. This issue is specifically focused at investigating performance improvements in Blazor's WebAssembly implementation for 5.0.

https://github.com/mono/mono is currently where the .NET WebAssembly team is tracking WASM specific issues. Since your issue pertains to the underlying platform, I'll recommend filing an issue there.

@yugabe When I have critical performance requirement and default blazor is slow I generate result html by myself dinamically, sent it to js using fast low level mono api and update dom from js.
It helps because it shows much faster render, about 10x faster depending on case.
I think it can be good for comparison too, until this way renders faster than blazor we have slow performance compared to js not wasm.
I think we can't really compare against wasm because wasm can't manipulate dom without js for now.
If you wonder how this technique works please check this video

@Lupusa87 I understand, but I feel it to be obvious that in the long run you have to have an out-of-the-box performant framework instead of a good enough manual workaround. It kind of defeats the purpose of Blazor itself to build a whole component in HTML and send the whole diff to JavaScript (you are overwriting a whole DOM subtree without considering which elements to keep, if I understood correctly). It is a good workaround, but it won't fix Blazor WASM on mobile. Have you tried using the @key attribute to speed up rendering though?

Now, Blazor WebAssembly performance is not quite good on mobile devices. Mostly just what you said: DOM manipulation is slow because there is no way to manipulate the DOM without using JS interop. If I know correctly, direct DOM manipulation is on the roadmap for WASM vNext. If someone is competent enough to help out the WASM guys a little, I'm sure it will be greatly appreciated to speed up the delivery of that feature.

@yugabe I use this technique only in critical parts and when it makes sense (when whole most part of dom is changing and no sense to analize changes which makes blazor slower) and shows better performance.
I do not like to do extra work and override blazor intended behavior but I am forced to do so in some cases, sure it is fast workaround and not solution but it works for me for now.

@SteveSandersonMS
I think blazor component should have option to re-render from scratch meaning not to do diffing, just remove old dom fragment and insert new one, it will save some time.
When blazor developer knows that some component on update has major changes he/she can turn this mode on.
Not sure if it is good idea but seems to be from my perspective.

something like this statehaschanged(true), statehaschanged can have bool parameter with default value false and if true is provided component will re-render full dom skipping diff calculations.

@Lupusa87 Now I understand your point. What you outlined just now could help in scenarios where a full component rerender could be preferred over diffing. This sounds a good compromise if no performance gains are available otherwise, a sort of band-aid, if you like. I feel it should be an advanced only scenario though, as the vast majority of the time you shouldn't have to worry about performance so much. Like now, when you are developing, say, an ASP.NET Core backend, you are going to have about 1% of the code being performance-critical, and 99% of the time "it just works". I feel something like this should be a baseline. Now, unfortunately, performance is sub-par, but the framework and foundation are very solid.

That's why I would recommend writing test applications using different alternative frameworks (Angular, React, plain JS, plain WASM) and compare performance to Blazor to find bottlenecks and possible optimization opportunities. I think that's the same method the ASP.NET Core backend team used to compare to NodeJS (but I'm just guessing).

Should the full component rerender be put into a separate issue?

Yes it is for advanced scenario and can be used in 1% of cases but it does not reduce it's importance.
Any framework's strengt is measured fow it can handle advanced scenarios, trivial ones can do everyone :)
Re dedicated issue I am not sure it makes sense bucause as I remember I am not saying this first time and probably the idea will be declined again with some right arguments which are hidden to my eyes.

For what Lupusa is saying, why not have a way to flag a component as render always with no diff built into the framework? Then for anything where performance is key, that attribute could be applied to force full render and replacement without doing any diffing? Or is this not really possible?

@radderz Perhaps it would be best not to get too deep in the details of this one particular scenario here, since this issue is meant to be tracking the whole range of perf-related work.

If you have a specific scenario that you want to optimize, could you please post a new separate issue rather than on this thread, giving an example of the code that you're trying to make faster, and what data you have that suggests that the problem is something specific (e.g., diffing)? Thanks very much!

@radderz @SteveSandersonMS There is a proposal (with unclear fate...) of having ability to provide control logic when to render at builder-API-level here: #21915 (further comment: https://github.com/dotnet/aspnetcore/issues/13610#issuecomment-631699616)

Was this page helpful?
0 / 5 - 0 ratings