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.16k stars 9.92k forks source link

Blazor Web App render mode interactive auto does not switch from interactive server to interactive wasm when navigating between pages #53799

Open lokitech opened 7 months ago

lokitech commented 7 months ago

Is there an existing issue for this?

Describe the bug

When using the default Blazor Web App template with interactivity set to auto and per page/component, first time visiting Counter page results in the page being rendered as InteractiveServer which is completely fine, after navigating to Home or to Weather page and then back to Counter, it is rendered in InteractiveWasm which is also great.

The problem occurs when I transfer the Weather page to Client project and set it to InteractiveAuto. After running the application and navigating to Counter or Weather page, the initial load happens in InteractiveServer which is also fine, but no matter how many times I navigate between Counter and Weather pages, it never makes a switch to InteractiveWasm as it does in the case when I navigate to Home and then back to Counter.

Expected Behavior

I would expect that after the WASM files are downloaded, the application would make a switch to InteractiveWasm when navigating between InteractiveAuto pages. As it does when I navigate from Counter to Home and back to Counter.

When navigating from Counter to Home a POST request is made to https://localhost/_blazor/disconnect with circuitId as payload which disconnects the WebSocket. image So when navigating back to the Counter page a GET request is made for blazor-hotreload.js which apparently switches the application from InteractiveServer to InteractiveWasm. image

So as far I can see, the functionality is there, is there a way to force the switch to InteractiveWasm when navigating from one InteractiveAuto to another InteractiveAuto page after the WASM files are downloaded?

Steps To Reproduce

  1. Create a new project with Blazor Web App template.
  2. Set Interactive render mode to Auto (Server and WebAssembly).
  3. Set Interactivity location to Per page/component.
  4. Delete Weather page from Server project and re-create it in Client project.
  5. Set Weather page @rendermode InteractiveAuto.
  6. Create DisplayRenderMode.razor component in Client project with following code:
    
    <p>Render Mode: @_renderMode</p>

@code { private string _renderMode = "Static server-side rendering";

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    if (firstRender)
    {
        _renderMode = OperatingSystem.IsBrowser() ? "Client-side rendering" : "Active server-side rendering";
        StateHasChanged();
    }
}

}


7. Add `<DisplayRenderMode />` to _Home_, _Counter_ and _Weather_ pages.
8. Launch the application.
9. Navigate to _Counter_, observe **Render Mode: Active server-side rendering**.
10. Navigate back and forth between _Counter_ and _Weather_ pages observing Render Mode not changing. 
11. Navigate to Home and then to _Counter_ or _Weather_ page, observe the change to **Render Mode: Client-side rendering**.

Here is the minimalistic project which reproduces this issue: https://github.com/lokitech/Blazor-Web-App-Render-Modes-Bug

### Exceptions (if any)

_No response_

### .NET Version

8.0.1

### Anything else?

_No response_
guardrex commented 7 months ago

@lokitech ... I think this is going to end up being by-design behavior. We've tried (and possibly failed 🙈) to address this situation at (second paragraph) ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#automatic-auto-rendering

... focusing on ...

Auto mode prefers to select a render mode that matches the render mode of existing interactive components.

Mackinnion's remarks are here ...

https://github.com/dotnet/aspnetcore/issues/52740#issuecomment-1852841365

I'll work that text further based on additional discussion here with the engineers. One of things that I'd like to flesh out in this scenario is exactly when does the router switch over to CSR ... what has to happen to break out of interactive SSR when navigating around Interactive Auto components that have initially adopted interactive SSR.

lokitech commented 7 months ago

Hi hello there, thank you for your reply. I would just like to say that I'm a bit perplexed with this decision, consider the following scenario:

Developer creates a Blazor Web App with render mode set to auto and interactivity location to global, meaning essentialy every page and compnent will have the render mode set to auto. First time users, will more than likely, be presented with server-side rendering while the WASM application downloads in the background, so their entirety of that first visit will be server-side rendered since:

Auto mode prefers to select a render mode that matches the render mode of existing interactive components

and auto mode is global. So in this case, only first time visitors and those whose cache has expired will be presented with server-side rendering, while others will always be using client-side rendering (considering there are no new updates published). Correct me if I'm wrong, but this seems to be an overhead of having a Blazor Server for both developing and maintenance for a miniscule return.

What I have measured is that initial load with server-side rendering is faster regardless WASM is cached or not. It would be ideal if every visit to the page is server-side rendered and then switched to client-side rendering as user navigates thourgh the application. I see the problem with application state not being passed to the client from server, but it is neither passed when navigating from interactive auto server-side rendered page to static server-side rendered page and back to interactive auto now client-side rendered page (counter -> home -> counter in my example in the first post). Am I missing something here?

MackinnonBuck commented 6 months ago

Hi @lokitech,

Consider the following scenario: Developer creates a Blazor Web App with render mode set to auto and interactivity location to global, meaning essentially every page and component will have the render mode set to auto. First time users, will more than likely, be presented with server-side rendering while the WASM application downloads in the background, so their entirety of that first visit will be server-side rendered...

Please correct me if I'm wrong, but I think your argument can be summarized by saying that since navigating from one InteractiveAuto page to another InteractiveAuto page results in an entirely new set of components, it's an ideal time to terminate the circuit and start using WebAssembly interactivity, because no component state will be lost. If that's what you're saying, then it should be noted that the "global interactivity" scenario is not equivalent to giving each page an InteractiveAuto render mode. Global interactivity works by making the router itself interactive, so there's never a transitional period during navigation where the set of interactive components completely changes (because it's the same interactive <Router /> component instance rendering each page). If we wanted to adopt the behavior you're describing for the global interactivity case, we'd have to allow InteractiveAuto components to dynamically change which interactive runtime they're using (so that the <Router /> can do this), which comes with other challenges that I think distract from the main issue here. Therefore, I'm going to continue by assuming that we're talking about an app with per-page interactivity, where multiple pages are configured to use the InteractiveAuto render mode. I hope that's a fine assumption to make and that I'm not missing another point you were making.

You're definitely right that there are cases where terminating the circuit and switching to WebAssembly interactivity is worthwhile, even if it means interactive state is lost. However, that's not always going to be the right choice, because the circuit might contain additional state that isn't strictly component state (DI services, etc.) that the app author wants to persist between navigations. You do bring up a good point that navigating to a non-interactive page terminates the circuit and loses interactive state anyway. We felt that it was worth it to dispose the circuit and its state if there aren't any interactive components around to immediately utilize that state, although I can appreciate that it's somewhat subjective to draw the line there.

Perhaps https://github.com/dotnet/aspnetcore/issues/48756 would allow the configuration of some of this behavior, or we could consider another way to allow explicit configuration of what happens when:

  1. The WebAssembly runtime has been completely downloaded,
  2. All interactive components on the current page use Server interactivity,
  3. An enhanced page update includes only InteractiveAuto components, and
  4. It can be determined by the client that each new InteractiveAuto component definitely does not correlate with an existing interactive component

If all these conditions are met, then we could consider terminating the circuit and allowing the InteractiveAuto render mode to resolve to WebAssembly.

lokitech commented 6 months ago

Hello @MackinnonBuck First off, thank you for your in-depth reply. Indeed I made a mistake by saying "global interactivity" translates to giving each page InteractiveAuto render mode. I was trying to make a point you summarized perfectly in the first paragraph.

As I've stated I understand the loss of component state along with DI services and everything in the application we are switching from. IMO that's even more of a reason to allow developers greater control over when the switch happens, so we (users-developers) can plan accordingly and tailor it for our use-cases.

I appreciate your time investigating this and considiring it for upcoming releases. Keep up the good work! Karlo