Skip to content

Commit a5ad244

Browse files
[release/8.0-rc2] [Blazor] Make auto components prefer the existing render mode (#50851)
# Make auto components prefer the existing render mode Ensures that if interactive components exist on the page and they all use a single render mode, components with the "Auto" render mode will also use that render mode. ## Description It's a common requirement for state to be shared between interactive root components. For example, the `<HeadOutlet>` component, which allows updating content in the HTML `<head>`, renders content specified by a `<HeadContent>` component, if it exists. However, this only works if the `<HeadOutlet>` and `<HeadContent>` components use the same interactive render mode. The Auto render mode lets Blazor select a "best" render mode (either Server or WebAssembly) at runtime, primarily depending on whether Blazor WebAssembly resources are cached in the browser. However, previously it did this without taking into consideration the render mode used by existing components on the page. For example, suppose a `<HeadOutlet>` with an Auto render mode gets added to the page before WebAssembly resources have been loaded and cached, so it uses Blazor Server. Later, a `<HeadContent>` component, also with an Auto render mode, gets added to the page. But at this point, WebAssembly resources have loaded completely, so the `<HeadContent>` component uses Blazor WebAssembly. In this example, the content in the `<head>` does not update, because the `<HeadOutlet>` and `<HeadContent>` use different render modes. This PR updates the auto render mode decision making logic to the following: * If WebAssembly components exist on the page, use WebAssembly * Otherwise, if Server components exist on the page, use Server * Otherwise, if WebAssembly resources are cached, use WebAssembly * Otherwise, use Server Fixes #50686 ## Customer Impact Medium/high. We've seen multiple reports of the existing behavior causing problems in customer apps, and there isn't a workaround. ## Regression? - [ ] Yes - [X] No This behavior has existed since the Auto render mode was introduced in an earlier .NET 8 preview release. ## Risk - [ ] High - [ ] Medium - [X] Low This PR is a small change to the Auto mode logic. It's unlikely that customers were already relying on the existing behavior. ## Verification - [X] Manual (required) - [X] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [X] N/A
1 parent a0c7939 commit a5ad244

File tree

3 files changed

+114
-18
lines changed

3 files changed

+114
-18
lines changed

src/Components/Web.JS/dist/Release/blazor.web.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Services/WebRootComponentManager.ts

+35-17
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type RootComponentInfo = {
3737
assignedRendererId?: WebRendererId;
3838
uniqueIdAtLastUpdate?: number;
3939
interactiveComponentId?: number;
40-
}
40+
};
4141

4242
export class WebRootComponentManager implements DescriptorHandler, RootComponentManager<never> {
4343
private readonly _rootComponents = new Set<RootComponentInfo>();
@@ -193,7 +193,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
193193
}
194194

195195
private circuitMayHaveNoRootComponents() {
196-
const isCircuitInUse = this.hasAnyExistingOrPendingServerComponents();
196+
const isCircuitInUse = this.rendererHasExistingOrPendingComponents(WebRendererId.Server, 'server', 'auto');
197197
if (isCircuitInUse) {
198198
// Clear the timeout because we know the circuit is in use.
199199
clearTimeout(this._circuitInactivityTimeoutId);
@@ -208,31 +208,38 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
208208

209209
// Start a new timeout to dispose the circuit unless it starts getting used.
210210
this._circuitInactivityTimeoutId = setTimeout(() => {
211-
if (!this.hasAnyExistingOrPendingServerComponents()) {
211+
if (!this.rendererHasExistingOrPendingComponents(WebRendererId.Server, 'server', 'auto')) {
212212
disposeCircuit();
213213
this._circuitInactivityTimeoutId = undefined;
214214
}
215215
}, this._circuitInactivityTimeoutMs) as unknown as number;
216216
}
217217

218-
private hasAnyExistingOrPendingServerComponents(): boolean {
219-
// If there are active Blazor Server components on the page, we shouldn't dispose the circuit.
220-
const renderer = getRendererer(WebRendererId.Server);
221-
if (renderer && renderer.getRootComponentCount() > 0) {
218+
private rendererHasComponents(rendererId: WebRendererId): boolean {
219+
const renderer = getRendererer(rendererId);
220+
return renderer !== undefined && renderer.getRootComponentCount() > 0;
221+
}
222+
223+
private rendererHasExistingOrPendingComponents(rendererId: WebRendererId, ...descriptorTypesToConsider: ComponentMarker['type'][]): boolean {
224+
if (this.rendererHasComponents(rendererId)) {
222225
return true;
223226
}
224227

225-
// If we have SSR components that may become Blazor Server components in the future,
226-
// we shouldn't dispose the circuit.
228+
// We consider SSR'd components on the page that may get activated using the specified renderer.
227229
for (const { descriptor: { type }, assignedRendererId } of this._rootComponents) {
228-
if (assignedRendererId === WebRendererId.Server) {
229-
// The component has been assigned to use Blazor Server.
230+
if (assignedRendererId === rendererId) {
231+
// The component has been assigned to use the specified renderer.
230232
return true;
231233
}
232234

233-
if (assignedRendererId === undefined && (type === 'auto' || type === 'server')) {
234-
// The component has not been assigned a renderer yet, so it's possible it might
235-
// use Blazor Server.
235+
if (assignedRendererId !== undefined) {
236+
// The component has been assigned to use another renderer.
237+
continue;
238+
}
239+
240+
if (descriptorTypesToConsider.indexOf(type) !== -1) {
241+
// The component has not been assigned a renderer yet, but it might get activated with the specified renderer
242+
// if it doesn't get removed from the page.
236243
return true;
237244
}
238245
}
@@ -298,9 +305,20 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
298305
}
299306

300307
private getAutoRenderMode(): 'webassembly' | 'server' | null {
301-
// If the WebAssembly runtime has loaded, we will always use WebAssembly
302-
// for auto components. Otherwise, we'll wait to activate root components
303-
// until we determine whether the WebAssembly runtime can be loaded quickly.
308+
// If WebAssembly components exist or may exist soon, use WebAssembly.
309+
if (this.rendererHasExistingOrPendingComponents(WebRendererId.WebAssembly, 'webassembly')) {
310+
return 'webassembly';
311+
}
312+
313+
// If Server components exist or may exist soon, use WebAssembly.
314+
if (this.rendererHasExistingOrPendingComponents(WebRendererId.Server, 'server')) {
315+
return 'server';
316+
}
317+
318+
// If no interactive components are on the page, we use WebAssembly
319+
// if the WebAssembly runtime has loaded. Otherwise, we'll wait to activate
320+
// root components until we determine whether the WebAssembly runtime can be
321+
// loaded quickly.
304322
if (hasLoadedWebAssemblyPlatform()) {
305323
return 'webassembly';
306324
}

src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs

+78
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,84 @@ public void AutoRenderMode_CanUseBlazorWebAssembly_WhenMultipleAutoComponentsAre
720720
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-2")).Text);
721721
}
722722

723+
[Fact]
724+
public void AutoRenderMode_UsesBlazorWebAssembly_WhenBothServerAndWebAssemblyComponentsExist()
725+
{
726+
Navigate($"{ServerPathBase}/streaming-interactivity");
727+
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
728+
729+
Browser.Click(By.Id(AddWebAssemblyPrerenderedId));
730+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
731+
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-0")).Text);
732+
733+
Browser.Click(By.Id(AddServerPrerenderedId));
734+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-1")).Text);
735+
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-1")).Text);
736+
737+
Browser.Click(By.Id(AddAutoPrerenderedId));
738+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-2")).Text);
739+
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-2")).Text);
740+
}
741+
742+
[Fact]
743+
public void AutoRenderMode_UsesBlazorServer_WhenOnlyServerComponentsExist_EvenAfterWebAssemblyResourcesLoad()
744+
{
745+
Navigate(ServerPathBase);
746+
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
747+
ForceWebAssemblyResourceCacheMiss();
748+
749+
Navigate($"{ServerPathBase}/streaming-interactivity");
750+
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
751+
752+
// We start by adding a WebAssembly component to ensure the WebAssembly runtime
753+
// will be cached after we refresh the page.
754+
Browser.Click(By.Id(AddWebAssemblyPrerenderedId));
755+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
756+
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-0")).Text);
757+
758+
Browser.Click(By.Id($"remove-counter-link-0"));
759+
Browser.DoesNotExist(By.Id("is-interactive-0"));
760+
761+
Browser.Navigate().Refresh();
762+
763+
Browser.Click(By.Id(AddServerPrerenderedId));
764+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-1")).Text);
765+
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-1")).Text);
766+
767+
// Verify that Auto mode will use Blazor Server, even though the WebAssembly runtime is cached
768+
Browser.Click(By.Id(AddAutoPrerenderedId));
769+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-2")).Text);
770+
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-2")).Text);
771+
}
772+
773+
[Fact]
774+
public void AutoRenderMode_UsesBlazorServer_AfterWebAssemblyComponentsNoLongerExist_ButServerComponentsDo()
775+
{
776+
Navigate($"{ServerPathBase}/streaming-interactivity");
777+
Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text);
778+
779+
Browser.Click(By.Id(AddWebAssemblyPrerenderedId));
780+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text);
781+
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-0")).Text);
782+
783+
Browser.Click(By.Id(AddServerPrerenderedId));
784+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-1")).Text);
785+
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-1")).Text);
786+
787+
Browser.Click(By.Id(AddAutoPrerenderedId));
788+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-2")).Text);
789+
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-2")).Text);
790+
791+
// Remove all WebAssembly components
792+
Browser.Click(By.Id("remove-counter-link-0"));
793+
Browser.Click(By.Id("remove-counter-link-2"));
794+
795+
// Verify that Blazor Server gets used
796+
Browser.Click(By.Id(AddAutoPrerenderedId));
797+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-3")).Text);
798+
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-3")).Text);
799+
}
800+
723801
[Fact]
724802
public void Circuit_ShutsDown_WhenAllBlazorServerComponentsGetRemoved()
725803
{

0 commit comments

Comments
 (0)