Why are Blazor lifecycle methods getting executed twice?

17,401

Solution 1

TL;DR

This is because of the prerendering mechanism that initializes the component as a part of the host page _Host.cshtml, so that the first http request would result in a host page that comes not only as a script loader of the blazor application, but also with a statically rendered view. Therefore the user could see the initial view without having to wait for roughly the following steps:

  • A WebSocket connection is established by SignalR.

  • The first bunch of render instructions are received from the server.

  • The render instructions are applied to the view.

This would not only shorten the responding delay before the user see the initial view, but also benefit SEO. The prerendered view would be replaced by the real blazor component after the blazor application normally starts.

The prerendering feature is enabled by default within the new project template, so you have to choose one of the followings:

  • Correctly handle the case that that component is prerendered (probably by checking whether the IJSRuntime is able to be resolved from the dependency injection).

  • Disable the prerendering feature by modifying the _Host.cshtml, replacing

<component type="typeof(App)" render-mode="ServerPrerendered" />

with

<component type="typeof(App)" render-mode="Server" />

For legacy versions, replace

@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))

with

@(await Html.RenderComponentAsync<App>(RenderMode.Server))

The Original Answer

I did a test with a fresh new blazorserver project, logging when lifecycle methods are called, and got this output:

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
      User profile is available. Using 'C:\Users\Alsein\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\repos\HelloWorld
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/_Host'
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[3]
      Route matched with {page = "/_Host"}. Executing page /_Host
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[103]
      Executing an implicit handler method - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[104]
      Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
crit: HelloWorld.MyBase[0]
      OnInitializedAsync
crit: HelloWorld.MyBase[0]
      OnParameterSetAsync
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[4]
      Executed page /_Host in 122.3724ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/_Host'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 216.7341ms 200 text/html; charset=utf-8
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/site.css
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/_framework/blazor.server.js
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/bootstrap/bootstrap.min.css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/site.css'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\site.css'
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/_framework/blazor.server.js'. Physical path: 'N/A'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 44.733000000000004ms 200 text/css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/bootstrap/bootstrap.min.css'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\bootstrap\bootstrap.min.css'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 55.3613ms 200 text/css
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 55.569900000000004ms 200 application/javascript
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/open-iconic/font/css/open-iconic-bootstrap.min.css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/open-iconic/font/css/open-iconic-bootstrap.min.css'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\open-iconic\font\css\open-iconic-bootstrap.min.css'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 4.5189ms 200 text/css
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:5001/_blazor/negotiate text/plain;charset=UTF-8 0
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/open-iconic/font/fonts/open-iconic.woff
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/open-iconic/font/fonts/open-iconic.woff'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\open-iconic\font\fonts\open-iconic.woff'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 4.3562ms 200 application/font-woff
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/_blazor/negotiate'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/_blazor/negotiate'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 24.7409ms 200 application/json
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:5001/_blazor?id=7oyJvbydrUy9tqlsH_DHzQ
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/_blazor'
crit: HelloWorld.MyBase[0]
      OnInitializedAsync
crit: HelloWorld.MyBase[0]
      OnParameterSetAsync

From the result we can see that, the component is loaded twice.

  • The first time it was loaded as a simple Mvc component directly when the page is requested and handled by /_Host which must be specified with the following code in _Host.cshtml, which calls the lifecycle methods for the first time:
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
  • Then resources are loaded including blazor.server.js.

  • Then the blazor app starts rendering.

  • Then the component is loaded as a blazor component, where the lifecycle methods are called for the second time.

Try replacing RenderMode.ServerPrerendered with RenderMode.Server, then it behaves as expected, which is that the lifecycle methods are only called for once (when the blazor app starts).

Conclusion: The default RenderMode is ServerPrerendered which must mean that Mvc could render the components as static contents in order to display the page contents before the blazor app is downloaded and starts, then when the blazor app starts, it takes over the page content. This must be a workaround for user experience that the browser user could wait less time for seeing the contents.

Solution 2

I had exactly the same issue with a holding page for a site where I had a little css animation which looked great in development, but when I put it live it ran twice. Changing the RenderMode to Server certainly fixes the issue, but it appears notably slower.

Funny because I would never have picked this up until I did this, for the final site I'll be switching back to ServerPrerendered

Share:
17,401

Related videos on Youtube

mko
Author by

mko

Updated on June 04, 2022

Comments

  • mko
    mko almost 2 years

    So with a release of asp.net core 3.0 and blazor 1.0 I started doing some actual work with blazor. When splitting Blazor component code into code behind I am using the following

     public class LogoutModel : BlazorComponent
        {
    }
    

    Unfortunatelly BlazorComponent does not exist anymore, so I move to ComponentBase. Not sure when did this change took place..

    Now the rest of my code looks like this

     public class LogoutModel : ComponentBase
        {
    
            protected override async Task OnInitializedAsync()
            {
            }
    
            protected override async Task OnParametersSetAsync()
            {
            }
        }
    

    What i notice is that life cycle methods are executed in the following order OnInitializedAsync() OnParametersSetAsync() OnInitializedAsync() OnParametersSetAsync()

    I am not really sure why is each method executed twice.

    This is what my Blazor file looks like

    @page  "/account/logout"
    @inherits LogoutModel
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <meta name="viewport" content="width=device-width" />
        <title></title>
    </head>
    <body>
        Logout page
    </body>
    </html>
    
    • Alsein
      Alsein over 4 years
      Which hosting model are you using?
    • mko
      mko over 4 years
      I am usimng blazor Server
  • Aurelien B
    Aurelien B about 4 years
    Thanks for taking the time to look into this, that helped me a lot.
  • Jordan Ryder
    Jordan Ryder almost 4 years
    Thanks, here's the MS Documentation
  • Paul-Sebastian
    Paul-Sebastian about 3 years
    If this were Nuxt.js, this would be similar to hybrid mode, where some part of the component or page can be controllably pre-rendered at the server, to speed things up, or use some service that is only available server-side (or should only be). There must be a way to check where the component is rendered, in the lifecycle method, I think I saw it somewhere but I don't remember it now. You could then simply do an if check and proceed accordingly.
  • Jon
    Jon almost 3 years
    I switched to "Server' but still calls twice. protected override async Task OnInitializedAsync() { await GetData() } ???
  • Khalid Ab
    Khalid Ab over 2 years
    if i can add something, this is what you have to change exactly in the _Host.cshtml file on the <app> element : render-mode="ServerPrerendered" to render-mode="Server" --- see here
  • Alsein
    Alsein over 2 years
    @KhalidAb The answer was written before the first GA version of blazor was released. The answer is updated.
  • David Bouška
    David Bouška over 2 years
    @Jon - see my answer below - it fixes your issue