From 231b0aaa908afa69794a8202608387e223d67281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Thu, 4 Sep 2025 11:00:31 +0200 Subject: [PATCH 1/2] Compare preload attributes --- .../src/Rendering/DomMerging/AttributeSync.ts | 2 +- .../Web.JS/src/Rendering/DomMerging/DomSync.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts b/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts index 09870ed75668..0e945180e2ef 100644 --- a/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts +++ b/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts @@ -43,7 +43,7 @@ export function synchronizeAttributes(destination: Element, source: Element) { } } -function attributeSetsAreIdentical(destAttrs: NamedNodeMap, sourceAttrs: NamedNodeMap): boolean { +export function attributeSetsAreIdentical(destAttrs: NamedNodeMap, sourceAttrs: NamedNodeMap): boolean { const destAttrsLength = destAttrs.length; if (destAttrsLength !== sourceAttrs.length) { return false; diff --git a/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts b/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts index 2707f3755a63..3a790f443478 100644 --- a/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts +++ b/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts @@ -5,7 +5,7 @@ import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor import { isInteractiveRootComponentElement } from '../BrowserRenderer'; import { applyAnyDeferredValue } from '../DomSpecialPropertyUtil'; import { LogicalElement, getLogicalChildrenArray, getLogicalNextSibling, getLogicalParent, getLogicalRootDescriptor, insertLogicalChild, insertLogicalChildBefore, isLogicalElement, toLogicalElement, toLogicalRootCommentElement } from '../LogicalElements'; -import { synchronizeAttributes } from './AttributeSync'; +import { attributeSetsAreIdentical, synchronizeAttributes } from './AttributeSync'; import { cannotMergeDueToDataPermanentAttributes, isDataPermanentElement } from './DataPermanentElementSync'; import { UpdateCost, ItemList, Operation, computeEditScript } from './EditScript'; @@ -308,8 +308,15 @@ function domNodeComparer(a: Node, b: Node): UpdateCost { return UpdateCost.Infinite; } - // Always treat "preloads" as new elements. - if (isPreloadElement(a as Element) || isPreloadElement(b as Element)) { + // If both elements are the same preload (based on all attributes), we need to match them and do nothing; + // Otherwise, browser would trigger a new preload request. + // If attributes don't match, we can't simply update the element, because browser could trigger + // an invalid preload request based on attribute order. + const aIsPreload = isPreloadElement(a as Element); + const bIsPreload = isPreloadElement(b as Element); + if (aIsPreload && bIsPreload && attributeSetsAreIdentical((a as Element).attributes, (b as Element).attributes)) { + return UpdateCost.None; + } else if (aIsPreload || bIsPreload) { return UpdateCost.Infinite; } From fecba44e611e0bb44186e303bf648d378b3f72ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Thu, 4 Sep 2025 13:53:02 +0200 Subject: [PATCH 2/2] Disable preloading for enhanced navigation --- .../EndpointHtmlRenderer.Streaming.cs | 2 +- .../src/Rendering/SSRRenderModeBoundary.cs | 5 +++++ .../src/Rendering/DomMerging/AttributeSync.ts | 2 +- .../Web.JS/src/Rendering/DomMerging/DomSync.ts | 18 +----------------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index ac6d24419895..50728a8c3271 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -324,7 +324,7 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo } } - private static bool IsProgressivelyEnhancedNavigation(HttpRequest request) + internal static bool IsProgressivelyEnhancedNavigation(HttpRequest request) { // For enhanced nav, the Blazor JS code controls the "accept" header precisely, so we can be very specific about the format var accept = request.Headers.Accept; diff --git a/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs index e9930ea24991..1febaa8f4af5 100644 --- a/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs +++ b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs @@ -124,6 +124,11 @@ public Task SetParametersAsync(ParameterView parameters) private void PreloadWebAssemblyAssets() { + if (EndpointHtmlRenderer.IsProgressivelyEnhancedNavigation(_httpContext.Request)) + { + return; + } + var preloads = _httpContext.GetEndpoint()?.Metadata.GetMetadata(); if (preloads != null && preloads.TryGetAssets("webassembly", out var preloadAssets)) { diff --git a/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts b/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts index 0e945180e2ef..09870ed75668 100644 --- a/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts +++ b/src/Components/Web.JS/src/Rendering/DomMerging/AttributeSync.ts @@ -43,7 +43,7 @@ export function synchronizeAttributes(destination: Element, source: Element) { } } -export function attributeSetsAreIdentical(destAttrs: NamedNodeMap, sourceAttrs: NamedNodeMap): boolean { +function attributeSetsAreIdentical(destAttrs: NamedNodeMap, sourceAttrs: NamedNodeMap): boolean { const destAttrsLength = destAttrs.length; if (destAttrsLength !== sourceAttrs.length) { return false; diff --git a/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts b/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts index 3a790f443478..b86ff3bd8d03 100644 --- a/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts +++ b/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts @@ -5,7 +5,7 @@ import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor import { isInteractiveRootComponentElement } from '../BrowserRenderer'; import { applyAnyDeferredValue } from '../DomSpecialPropertyUtil'; import { LogicalElement, getLogicalChildrenArray, getLogicalNextSibling, getLogicalParent, getLogicalRootDescriptor, insertLogicalChild, insertLogicalChildBefore, isLogicalElement, toLogicalElement, toLogicalRootCommentElement } from '../LogicalElements'; -import { attributeSetsAreIdentical, synchronizeAttributes } from './AttributeSync'; +import { synchronizeAttributes } from './AttributeSync'; import { cannotMergeDueToDataPermanentAttributes, isDataPermanentElement } from './DataPermanentElementSync'; import { UpdateCost, ItemList, Operation, computeEditScript } from './EditScript'; @@ -308,18 +308,6 @@ function domNodeComparer(a: Node, b: Node): UpdateCost { return UpdateCost.Infinite; } - // If both elements are the same preload (based on all attributes), we need to match them and do nothing; - // Otherwise, browser would trigger a new preload request. - // If attributes don't match, we can't simply update the element, because browser could trigger - // an invalid preload request based on attribute order. - const aIsPreload = isPreloadElement(a as Element); - const bIsPreload = isPreloadElement(b as Element); - if (aIsPreload && bIsPreload && attributeSetsAreIdentical((a as Element).attributes, (b as Element).attributes)) { - return UpdateCost.None; - } else if (aIsPreload || bIsPreload) { - return UpdateCost.Infinite; - } - return UpdateCost.None; case Node.DOCUMENT_TYPE_NODE: // It's invalid to insert or delete doctype, and we have no use case for doing that. So just skip such @@ -331,10 +319,6 @@ function domNodeComparer(a: Node, b: Node): UpdateCost { } } -function isPreloadElement(el: Element): boolean { - return el.tagName === 'LINK' && el.attributes.getNamedItem('rel')?.value === 'preload'; -} - function upgradeComponentCommentsToLogicalRootComments(root: Node): ComponentDescriptor[] { const serverDescriptors = discoverComponents(root, 'server') as ServerComponentDescriptor[]; const webAssemblyDescriptors = discoverComponents(root, 'webassembly') as WebAssemblyComponentDescriptor[];