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.21k stars 9.95k forks source link

Blazor InteractiveServer takes twice as long to create a page #53501

Open DavidThielen opened 8 months ago

DavidThielen commented 8 months ago

Hi all;

First off, this is the wrong category for Blazor issues, please tell me the proper location.

Second, while there is great documentation about how InteractiveServer works and is used, there is almost none about why you all took this new approach. The best I got was this discussion.

As I understand it the goal is twofold:

  1. Get something up ASAP so a user does not get bored and leave the page.
  2. Get the complete static page up ASAP for crawlers.

These are both great goals. Unfortunately InteractiveServer mode will call OnInitializedAsync() twice, sometimes. This means reading the data to populate the page has to be done twice. And this means it takes twice as long to have the static web page delivered. And because this happens sometimes, you can't even write code to do the fast activity the first call and the slow (calling the DB) the second call.

As this is counter to the two goals listed above (please correct me if those are not the goals that drove InteractiveServer), I believe that this double call should be viewed as a bug and fixed.

As an alternative, you could consider instead having three calls, each called exactly once:

  1. OnInitializedFastPartAsync
  2. OnInitializedEverythingElseAsync
  3. OnTwoWayCommunicationEstablishingAsync
  4. OnTwoWayCommunicationEstablishedAsync

I'm not really sure if you need the third and/or fourth one. But breaking the initialization into two calls, each of which is called exactly once, would eliminate the present issue where pages take twice as long to load.

But at a minimum, please eliminate the sometimes second call to OnInitializedAsync() - doubling page load times is really bad.

thanks - dave

MariovanZeist commented 8 months ago

Hi @DavidThielen

I am guessing this is due to Prerendering

You can disable Prerendering If you don't need it. Change the Routes in your `app.razor file to

 <Routes @rendermode="new InteractiveAutoRenderMode(prerender: false)" />

or

 <Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

Depending on which RenderMode you are using.

This will globally disable prerendering on your app.

DavidThielen commented 8 months ago

@MariovanZeist Thank you for answering. But I still think this is a serious issue. For cases where they want to render the HTML quickly (which we all would prefer), they're left with the trade-off that they will be reading their data twice. This should be changed so we can get the fast rendering and only read our data once.

As to your suggestion, the link you provided says two directly contradictory things. First it says:

Making a root component interactive, such as the App component, isn't supported. Therefore, prerendering can't be disabled directly by the App component.

It then goes on to say:

For apps based on the Blazor Web App project template, a render mode assigned to the entire app is specified where the Routes component is used in the App component (Components/App.razor). The following example sets the app's render mode to Interactive Server with prerendering disabled:

So it says you can't set this in App.razor. Then tells you to turn it off globally, set it to false in App.razor. If that works, great. Except you're then left worried that a change will be made to enforce the prohbition on doing this. So at best, worrisome.

javiercn commented 8 months ago

@DavidThielen thanks for contacting us.

I don't think your expectations match the expectations of most web developers. As I pointed out on the issue, you can disable prerendering if you don't need it/want it, but it's not what most people want.

Unfortunately, InteractiveServer mode will call OnInitializedAsync() twice, sometimes

This is not accurate; it will always render once on the server (statically) and once inside the circuit (interactively). You can easily see this by putting a breakpoint and looking at the callstack window on VS to see the different paths.

As for prerendering, you can use Streaming Rendering to avoid waiting for an async task to complete (querying your Db) before the server starts sending the response (that way, you get a skeleton on the page, and then when the data is ready, an update is sent and the page updates as part of the same response).

Finally, even if you don't use streaming rendering, you still benefit from prerendering as the whole content is delivered on the first response, as opposed to after a cascade of 4 requests which is roughly what it takes to establish a Blazor server connection. (Load the page, load the blazor script, invoke the _negotiate endpoint, open the WebSocket and render the content). So if you have an RTT of 200ms we are talking about a difference between 200ms to 800+ms to render the initial content (probably more as the content needs to be downloaded, parsed, etc., which also takes time).

Finally, prerendering offers the benefit that the content is with a very high chance, available to your users even if something fails along the way.

With all this in mind, the only thing that matters here is whether the experience is acceptable for your users or not. If you are serving a handful of corporate clients over an intranet, with reliable network connections, and who don't have alternatives (they don't have any other choice but to use your app), it might not matter to you, and you can simply disable it.

For the most part, it shouldn't matter whether you disable it or not. If you have an expensive operation as part of rendering, it might be triggered twice for a given session, but the same can happen if the user presses F5 to refresh, so the answer in that situation is to cache the data.

DavidThielen commented 8 months ago

@javiercn I agree with most everything you said. It is a great idea to get the HTML skeleton delivered to the client as quickly as possible. I say the advantage of this firsthand when I was using Star Link for a month and a lot of websites I hit just died over the latency.

Where we disagree is that the way this is presently handled is to call OnInitializedAsync() for that initial skeleton render. As there's no way to programmatically determine if it's the skeleton call, I have to call the DB to load all the data. This unnecessarily slows down the completion of that first OnInitializedAsync() call, which slows down the initial render. And it doubles the amount of total initializing processing time, doubling how long it takes to display the final populated page.

I propose that instead you add the event OnSkeletonAsync() and that replaces the first call to OnInitializedAsync(). If you did that, then every advantage you list above will be true, with none of the disadvantages I listed.

And if I'm not understanding something about all this, please explain. The Blazor designers are clearly very smart so this double call surprises me. If there's something about it that does not cause the skeleton render to take longer and does not double the total initialize time, please enlighten me. (One big problem with this new mode is while there is good documentation about how to use it, there's nothing I can find that explains why this approach was taken and how the skeleton pass works if OnInitializedAsync() remains a slow heavy method.

thanks - dave

ps - Also, if there's something that explains what streaming rendering is truly doing, and how best to work with it. A link to that would be very enlightening too. This doesn't explain it, it just says it will feed content as it can. Keep in mind, the better we programmers understand what is truly going on, the better we can write out code to leverage it.

BrendanRidenour commented 8 months ago

Thanks for this discussion. This is an issue I've encountered myself recently while developing with Blazor. Ultimately, I share the positions of @DavidThielen . It surprised me that OnInitializedAsync was called twice. Digging deeper, I do understand why, but it left me with no great options.

Either let my database (and anything else) get hit twice to fetch identical data, or disable prerendering and lose its benefits. I'd really appreciate a built-in way to get the best of both concerns.

garrettlondon1 commented 5 months ago

One thing I never understood about the OnInitializedAsync, OnAfterRenderAsync + Prerendering combination:

Isn't the second call of OnInitialized, in a prerender scenario, the same as the firstRender run in OnAfterRenderAsync(bool firstRender)?

What is the difference here? Why would someone wanting to take advantage of Prerendering ever want to use OnInitialized. Wouldn't the correct lifecycle be:

Prerender static HTML skeleton OnAfterRender(firstRender = true)