Replies: 6 comments 32 replies
-
The changes above are mostly complete - the SiteService has been refactored to have client and server implementations, and the App component now uses the SiteService to retrieve information using the server implementation and passes the PageState to the SiteRouter - which eliminates a number of data access calls on a full page request when running on Static or Interactive render mode. One of the challenges I am working on currently is related to the Head component. In Interactive rendering this component is responsible for injecting the favicon, page title, as well as any arbitrary head content (ie. meta, link references, etc...) based on the configuration in your site. This component is also what allows modules to dynamically set the page title, etc... It accomplishes this using the INotifyPropertyChanged concept which is part of Blazor. Unfortunately, this approach does not seem to work in Static rendering. There are a number of issues logged in the aspnetcore repo related to HeadOutlet - which suffers from a similar problem. Basically the problem is that although the Head component receives the INotifyPropertyChanged events, it does not render the markup in the UI. This seems to be related to "mixed mode" scenarios ie. the app loads in Static render mode by default and has a few components which are Interactive. In this configuration the DOM differencing algorithm gets confused and actually removes elements from the DOM which were added during static/pre rendering (see dotnet/aspnetcore#50268). Again this is part of the Blazor "black box" which makes it difficult to diagnose and find workarounds. This is a fairly major problem as it means that favicon, page title, as well as any arbitrary head content (ie. meta, link references, etc...) are not being injected into the page - which are some of the most critical elements you want to include in a static rendering scenario (for SEO). I initially tried implementing some logic in the App component to include elements in the page head and although it would work for configuration stored in the site, it would not support the dynamic ability for a module to set the title, etc... So more research is required. This link offers some advanced render mode concepts but I have not been able to get any of them to work for this specific scenario - https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#fine-control-of-render-modes Another challenge is related to module and theme scripts and stylesheets. In Interactive rendering, scripts and stylesheets are handled by JS Interop in OnAfterRender() - which is implemented in ModuleBase, ThemeBase, etc... In Static rendering you can't use JS Interop and the OnAfterRender() event is not even called so a different approach is needed. Rather than having 2 totally different implementations I am hoping I can find a happy medium. I am currently experimenting with calling the JavaScript LoadJS methods in the traditional way when running on Static rendering (rather than using JS Interop). This would provide consistency on how scripts are injected into the DOM - they would just be triggered in different ways by the framework based on the render mode. |
Beta Was this translation helpful? Give feedback.
-
I have not been able to make any progress on the INotifyPropertyChanged issue with Static rendering. If you want to experiment with the problem yourself open the Head.razor file and put some breakpoints on the various methods. Also put a breakpoint in ThemeBuilder,razor where it sets the page title:
When you run the app you will see the OnInitialized() method in Head.razor get called first, registering the PropertyChanged event. Then you will see the ThemeBuilder.razor logic called where it sets the PageTitle. Then the Head.razor PropertyChanged event will fire and you can see the _title property get set with a value. This should be rendered in App.razor where the Head.razor is declared:
However the markup is never rendered into the page output: Note that if you set the Site to use Interactive render mode, this approach works fine and includes the page title: The strange thing is that is that this same problem exists in the Oqtane SSR POC... however I also created another Blazor Web App with per component rendering and added a service with implements INotifyPropertyChanged to it... and it renders the content to the UI as I would expect. So I am trying to determine what the difference is which is causing the problem. NotifyProperty.cs
Program.cs
Sender.razor
Receiver.razor
App.razor
Home.razor
Counter.razor
Results in: |
Beta Was this translation helpful? Give feedback.
-
@thabaum in regards to your earlier comment "I notice the blinking in Static Server that does not exist as much or at all in Interactive Server render mode. Like things are loading twice when visiting the interactive admin component pages. Page Management I think along with System Info seem to be the smoothest or more expected behavior." - I do have some theories about this which I will need to explore tomorrow. Basically in interactive rendering, Oqtane is relying on custom JavaScript to efficiently manage the injection of stylesheets... there was a long discussion about this last year when we upgraded to .NET 7 because we tried to migrate to using the new Head component approach but it did not provide a good user experience we so rolled back to the custom method. In static rendering I think something similar is happening where the stylesheet references are being removed and then re-added on every page load - which is resulting in the behavior you are seeing in the browser. |
Beta Was this translation helpful? Give feedback.
-
@sbwalker odd behavior detected while setting themes on different pages, then navigating between the pages, you have to refresh the page each time you click on a navigation. Almost like if the page theme is different then the site theme a reload or refresh needs to happen. Only in static SSR. I suppose I will log the issue. |
Beta Was this translation helpful? Give feedback.
-
There are a number of places within Oqtane where a component needs to redirect to another Url. In Interactive rendering if you placed the call to NavigationManager.NavigateTo() within OnParametersSet() it would throw an exception. So the standard guidance was to include the NavigationManager.NavigateTo() logic in OnAfterRender(). On Static rendering OnAfterRender() is not a life cycle event so it will not be invoked. This means that coditional logic needs to be added in OnParametersSet():
and the traditional NavigationManager.NavigateTo() logic can be retained in OnAfterRender() for interactive scenarios (no conditional logic needed here because it is only called in interactive render mode). I am not a big fan of having to sprinkle conditional logic around the app based on render mode but at least it is simple to handle in the cases where it is necessary because Oqtane provides the PageState - standard Blazor in .NET 8 does not provide a way to determine the render mode out of the box (it is one of the enhancement requests for .NET 9). |
Beta Was this translation helpful? Give feedback.
-
Even with the improvements outlined at the top of this thread, the run-time characteristics of static rendering are much different than interactive rendering. This is because static rendering is stateless - which means methods such as SiteService GetSite() need to be called on every browser request to load the data from the server (whereas interactive rendering is stateful and can re-use data loaded previously). Caching had been implemented in prior releases within the SiteService GetSite() method for unauthenticated users which eliminated the bulk of database calls for public-facing scenarios. This was possible because all unauthenticated users have the same set of permissions in terms of the content they are allowed to access. However, this does not provide any benefit for authenticated users. Caching data for each authenticated user is not difficult... the challenge is dealing with cache invalidation when the site configuration changes at run-time, or when the user's permissions are updated. This required some improvements to the caching infrastructure to provide more context and control over cache items. With the latest changes, authenticated users will get the same performance benefits as unauthenticated users, and the cache will be refreshed immediately when information changes. |
Beta Was this translation helpful? Give feedback.
-
The previous thread (#3698) is getting too long, so starting a new one.
Now that most of the UI use cases are working, it is time to focus on the runtime workload characteristics...
When running on Interactive rendering, the App server component is executed once for every unique browser session. It makes a number of database calls to retrieve information so that it can populate the initial page markup. It then initiates the component rendering process which intantiates the SiteRouter. The SiteRouter makes a number of Service API calls (using HttpClient) to populate the PageState. On each subsequent user interaction the SiteRouter utilizes the existing PageState so that it does not have to reload information.
When running on Static rendering currently, the App server component is executed for every request (ie. every user interaction). It makes a number of database calls to retrieve information so that it can populate the initial page markup. It then initiates the component rendering process which intantiates the SiteRouter. The SiteRouter makes a number of Service API calls (using HttpClient) to populate the PageState. On each subsequent request (ie. user interaction) the SiteRouter has to reload the PageState.
So based on above, it is clear that when running on Static rendering the framework currently has a much higher workload than it does on Interactive rendering. In fact, on each request it is loading the same information multiple times (ie. in both the App and SiteRouter components). In addition, the SiteRouter is relying on Service API calls (using HttpClient) which is not necessary in Static rendering, as this workload is running on the server.
In order to address the runtime characteristics above, I am going to isolate the workload which is currently in SiteRouter and move it to App. App will construct the PageState and will pass it as a component parameter to Routes which will then pass it to SiteRouter. SiteRouter will then use the PageState if it is provided (which will not always be the case as if you are running on .NET MAUI then App is never executed). This will eliminate the "double workload" which currently exists for Static rendering. It will also benefit Interactive rendering (to a lesser extent as App and SiteRouter are not being executed frequently in interactive scenarios - however if you are running on Globally Interactive public site which contains a lot of content pages then this improvement will be beneficial).
I will also be implementing the approach which was demonstrated in the Oqtane SSR POC which utilizes dependency injection to vary the implementation of Service APIs based on where they are executing (ie. server or client). This will allow server-based workloads to avoid the HttpClient overhead... and client-based workloads to continue to function as they do today.
Again, I am giving a heads up about these changes because they are invasive to the framework and may result in some instability in the Dev branch for a period of time.
Beta Was this translation helpful? Give feedback.
All reactions