Repository navigation
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
22b9b30 to
53ce2a0
Compare
JeanMeche
left a comment
There was a problem hiding this comment.
AGENT: Addressing redundant DOM writes during hydration in @angular/core is the right architectural direction (and addresses the root cause behind issues like #71872 and #71873). However, there are a few correctness and design issues with how setupStaticAttributes and isDomPropertyUnchanged currently check the DOM—see inline comments below.
| export function setupStaticAttributes( | ||
| renderer: Renderer, | ||
| element: RElement, | ||
| tNode: TNode, |
There was a problem hiding this comment.
AGENT: Running setupStaticAttributes with skipIfUnchanged during hydration (creation mode) compares the SSR DOM against the static tNode attributes/classes/styles and overwrites them whenever they differ. Because creation mode runs before directive ngOnInit and update-mode bindings ([class.*], [style.*], [attr.*]), any attribute, class, or style that was legitimately augmented on the server by a directive or binding will differ from the static tNode value and get clobbered during hydration:
- Combined static + dynamic
class/style: For<div class="static" [class.active]="true">, the SSR HTML hasclass="static active". In creation mode,element.getAttribute('class') !== classes('static active' !== 'static') evaluates totrue, sowriteDirectClassclobbers the attribute back to'static', only for the update pass to re-add'active'immediately after—resulting in two unnecessary DOM mutations during hydration. - Directive-modified attributes (e.g.
NgOptimizedImagein NgOptimizedImage makes the browser select each image source again during hydration #71873): For<img ngSrc="hero.jpg" width="400" height="300" sizes="50vw">, SSR renderssizes="auto, 50vw". Herenative.getAttribute('sizes') !== '50vw'istrue, sosetupStaticAttributesoverwritessizesback to'50vw', andNgOptimizedImage.ngOnInit()then writes'auto, 50vw'again.
When hydrating an existing element (!wasLastNodeCreated()), the server has already serialized its static attributes, classes, and styles. Have we considered skipping setupStaticAttributes altogether when !wasLastNodeCreated() (aside from isAnimationProp properties which aren't serialized as HTML attributes)? That would avoid all the getAttribute reads on static attributes and prevent clobbering directive/binding state from SSR.
There was a problem hiding this comment.
Those writes still happen, as they do today: static active differs from the template, so it's replaced, and [class.active] adds the class back.
I'd keep the comparison rather than skip setupStaticAttributes altogether, because skipping is what #71200 does, and your review there lists what it breaks. With the comparison, a server-only class or a value from an older deployment is still corrected, animation properties are still set, and an ngSkipHydration host is still written when it differs. Root hosts aren't touched.
The cost is that the sizes case stays as it is. #71873 tracks that one.
There was a problem hiding this comment.
AGENT: Thanks for updating the PR and scoping down the dynamic property/attribute checks—narrowing isFirstWriteOfRenderedResource to video, audio, iframe, embed, and object[data] makes the property-binding side much safer and avoids the pitfalls with generic DOM property reflection and custom elements.
Regarding setupStaticAttributes:
Fair point about the subtle regressions identified in #71200 (such as root hosts, animation properties, and ngSkipHydration).
However, comparing element.getAttribute('class') !== classes and element.getAttribute('style') !== styles in creation mode has a fundamental limitation: it only checks static classes/styles against the SSR DOM, but the SSR DOM legitimately contains classes and styles added by update bindings ([class.*], [style.*]) and directives. On any element combining static classes with dynamic bindings (e.g. <div class="card" [class.active]="isActive">), getAttribute('class') is 'card active', which doesn't match 'card', so it still overwrites the class back to 'card' in creation mode, only for [class.active] to re-add 'active' right after.
Rather than trying to partially patch class and style reconciliation here (which is a much broader problem with styling pipelines and directives like in #71873):
What do you think about narrowing this PR specifically to the media/resource loading bug (#71872)?
- Keep your narrowed dynamic
src/datacheck inshared.ts(isFirstWriteOfRenderedResource). - In
setupStaticAttributes/setUpAttributes, guard only staticsrc/datafor these media elements (video,audio,iframe,embed,object), leavingclassandstyleuntouched.
That gives a very clean separation of concerns:
- This PR (fix(core): don't reload iframes and media during hydration #71874) 100% fixes the media restarting/reloading bug for elements managed by the framework (
<video>,<audio>,<iframe>) without touching the fragile styling pipeline. - PR perf(common): avoid re-setting unchanged attributes in NgOptimizedImage #71875 handles
NgOptimizedImagespecifically, which is genuinely needed becauseNgOptimizedImagemanages its attributes imperatively viaRenderer2.setAttributeinngOnInit(bypassing core's template binding pipeline entirely).
There was a problem hiding this comment.
Done. I narrowed the PR as you suggested and squashed it into one commit:
- Static values: only
srconiframe,embed,video, andaudio, anddataonobject, are compared.classandstyleare written as before. - Bindings: the same check, through one shared
isResourceAttribute.
I opened #71971 for the cost of the other writes, with the measurements and the class/style problem you described.
| } | ||
|
|
||
| /** Properties whose getter returns the resolved URL rather than the value that was set. */ | ||
| const URL_PROPERTIES = /^(src|href|poster|data|action|formAction|cite)$/; |
There was a problem hiding this comment.
AGENT: A few concerns with isFirstPassOfHydratedView and isDomPropertyUnchanged:
- View-scoped vs. node-scoped check:
isFirstPassOfHydratedView(lView)istruefor the entireLView, including DOM nodes inside that view that were created fresh on the client (e.g. inside anngSkipHydrationblock or unhydrated fallback nodes). On a newly created element, bindings like[title]="''"or[customProp]="undefined"will see(element as any)[propName] === valueastrueand skip callingrenderer.setProperty. - Customized built-in elements:
element.tagName.includes('-')only matches autonomous custom elements, missing customized built-in elements (<button is="my-button">) whose property setters would be skipped. - Type coercion &
URL_PROPERTIES: Non-string values bound to string DOM properties (e.g.<input [value]="123">) fail strict equality ('123' === 123isfalse), andURL_PROPERTIESdoesn't account for cases likeSVGElement.href(SVGAnimatedString). Renderer2boundary: Accessingelement.getAttribute/element.tagName/element[propName]directly incorebypassesRenderer2.
There was a problem hiding this comment.
I removed the generic property comparison. The second commit now covers only src on iframe, embed, video, and audio, and data on object.
- A node created on the client doesn't have the attribute, so it's still written. There's a spec for this.
- A customized built-in such as
<video is="my-video">with its ownsrcsetter would still be skipped. It's a narrow case, so I've left it out to keep the check small. A server-rendered customized built-in always has theisattribute, so an extrahasAttribute('is')check would cover it. Would you like me to add that? - The comparison is between the sanitized value as a string and the attribute, so it doesn't read a property. SVG isn't in the list.
- It still reads
getAttributedirectly, for those five elements only. Hydration already does the same inhydration/utils.ts.
| @Component({ | ||
| selector: 'app', | ||
| template: `<div class="static" [class]="classes"></div>`, | ||
| }) |
There was a problem hiding this comment.
AGENT: Note that this test (<div class="static" [class]="classes"></div>) only removes server-only because the presence of the static class="static" attribute causes setupStaticAttributes to overwrite class="static server-only" back to class="static" during creation mode. Without a static class attribute (<div [class]="classes"></div>), tNode.classes is null and server-only is not removed.
There was a problem hiding this comment.
Yes, that's the behavior the spec is meant to keep. I renamed it to say that it applies to an element with a static class. Without a static class, the server's class stays, as it does today.
53ce2a0 to
9a480b2
Compare
During hydration, Angular wrote the `src` of `<iframe>`, `<embed>`, `<video>`, and `<audio>` elements, and the `data` of `<object>` elements, again with the value that the server had rendered. The browser loads the resource again whenever one of these attributes is set, even to the same value: an iframe loads its document again and a playing video restarts. On elements reused from the server, core now leaves these attributes alone when they already have the value. This applies to static attributes outside ICU expressions, and to the first update of property and attribute bindings. A value that differs is still written, and later updates aren't affected. Fixes angular#52477
9a480b2 to
a5a00a3
Compare
PR Checklist
PR Type
What is the current behavior?
Fixes #52477
During hydration, Angular writes the
srcof<iframe>,<embed>,<video>, and<audio>, and thedataof<object>, again with the value that the server rendered. The browser loads the resource again whenever one of these attributes is set: an iframe loads its document again and a playing video restarts.What is the new behavior?
A resource URL is
srconiframe,embed,video, oraudio, ordataonobject(isResourceAttribute). The comparison runs after sanitization, on the string value. Every other attribute, class, style, and binding is written as before. #71971 tracks the cost of those writes.Does this PR introduce a breaking change?
I've left this open for you to decide. The final attributes don't change, but a few handlers that ran only because of the repeated write no longer run. See Merge Danger.
Other information
Evidence
Chrome 156, from the reproduction:
<video>withsrc,[src], or[attr.src]that was playing at 1.5 s<video>whosesrcdiffers between server and browser<iframe>withsrcor[src], documents loadedThese specs fail without the source change and pass with it:
The other new specs pass both ways. They cover:
class, andstyle, which are still written;loadon aniframe.Merge Danger
Door: two-way. No API or serialized format changes.
Blast Radius: hydrated apps with
iframe,embed,video,audio, orobjectelements.These handlers no longer run for an event that fired before hydration, because nothing fires it again:
(loadedmetadata),(canplay), and(ended)on a video that was ready before hydration. Today they run because the video restarts. Event replay doesn't deliver them: media events don't bubble, and replay listens for them in the bubble phase or not at all. Code can checkreadyStateinafterNextRenderinstead.loadon aniframethat are added imperatively (fromEvent,renderer.listen), andpostMessagehandshakes that rely on the second load.(load)on aniframein an app withwithNoIncrementalHydration()and nowithEventReplay().With event replay on, which is the default,
(load)on aniframeruns once instead of twice.An iframe that navigated before hydration is no longer sent back to its
src. The same goes for server HTML whosesandboxorallowdiffers but whosesrcmatches: the new policy only applies at the next navigation.Not covered:
<video is="my-video">, whose ownsrcsetter isn't called when the write is skipped. AhasAttribute('is')check would cover it, if you want one.