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. |
c700c0d to
029e495
Compare
|
This seems like a local fix of a much larger problem. |
| private setHostAttribute(name: string, value: string): void { | ||
| this.renderer.setAttribute(this.imgElement, name, value); | ||
| // The browser reacts to identical writes, e.g. by selecting the image source again. | ||
| if (this.imgElement.getAttribute(name) !== value) { |
There was a problem hiding this comment.
AGENT: This feels like a local workaround in NgOptimizedImage for the broader hydration issue being tackled in #71874:
- Calling
this.imgElement.getAttribute(name)here runs 7–8 synchronous DOM reads perNgOptimizedImageeven during normal client-side rendering (CSR), and bypassesRenderer2. - Even with this change, static
width,height, andsizesattributes on<img ngSrc ...>are still rewritten during hydration bysetupStaticAttributesin@angular/core(which is whywidthandheighthad to be excluded fromdirectiveAttrsinfull_app_hydration_spec.ts, and whysizes="auto, ..."still mutates twice in NgOptimizedImage makes the browser select each image source again during hydration #71873). - Standard
<img>tags withoutNgOptimizedImagesuffer from the exact same source re-selection and duplicateloadevent during hydration unless handled at the core/hydration level.
There was a problem hiding this comment.
You're right that this is local and that the larger problem remains. A plain <img> and static attributes on <img ngSrc> go through core, and this change doesn't touch them. #71971 tracks those writes.
#71874 now only skips resource URLs on <iframe>, <embed>, <video>, <audio>, and <object>, so it doesn't reach <img>. The directive writes src and srcset itself through Renderer2. Hydration time in ms, median of 7 page loads, from timings.txt:
| Page | Stock | Directive fix |
|---|---|---|
| 200 images, static attributes | 34.8 | 28.1 (−19%) |
| 200 images, bound attributes | 23.4 | 9.6 (−59%) |
On your three points:
- The reads add 96–164 ns per image in client-side rendering, or 19–33 µs for 200 images. The PR description has the microbenchmark. That's well under 1% of hydration time, so I didn't limit them to hydration.
Renderer2can't read attributes, andremovePlaceholderOnLoad()already reads the element directly. - Agreed. Static
width,height, andsizesare written bysetupStaticAttributesin core. - Agreed. A plain
<img>needs a fix in core.
If core ends up skipping identical writes in the renderer, this check can be removed, apart from the handling of failed images with a placeholder.
029e495 to
b8b0c91
Compare
During hydration, `NgOptimizedImage` wrote all of its host attributes again, with the values that the server had already rendered. The browser reacts to a write to `src`, `srcset`, or `sizes` even when the value doesn't change, and it selects the image source again. `setHostAttribute` now skips the write when the element already has the value. An image that still has a placeholder keeps its writes, so that an image that failed before hydration fires `error` again and loses its placeholder.
b8b0c91 to
5cce7a2
Compare
PR Checklist
PR Type
What is the current behavior?
Part of #71873
During hydration,
NgOptimizedImagewrites all of its host attributes again, with the values that the server rendered. The browser selects the image source again after a write tosrc,srcset, orsizes, even when the value doesn't change.What is the new behavior?
private setHostAttribute(name: string, value: string): void { - this.renderer.setAttribute(this.imgElement, name, value); + if (this.placeholder || this.imgElement.getAttribute(name) !== value) { + this.renderer.setAttribute(this.imgElement, name, value); + } }An image that still has a placeholder keeps today's writes. An image that failed before hydration then fires
erroragain, which removes the placeholder. An image that already loaded clears its placeholder before the writes, so it skips them.Does this PR introduce a breaking change?
The public API doesn't change. The visible differences are repeated events that were a side effect of the rewrite, described under Risks.
Other information
Evidence
On a page with 200 images, hydration takes 28.1 ms instead of 34.8 ms. With 4× CPU throttling, it takes 198.6 ms instead of 247.9 ms. With bound
[width],[height], and[sizes], it takes 9.6 ms instead of 23.4 ms.results/timings.txtin the reproduction has the measurements. The benchmark pages have no placeholders, so thethis.placeholdercondition doesn't affect them.The extra reads also run during client-side rendering. They add 96–164 ns per image in Chrome 157, which is 19–33 µs for 200 images, compared with about 175 µs per image for hydration on the page above. So they aren't limited to hydration.
Microbenchmark
Each sample runs the 8 writes of
setHostAttribute()on 50,000 new images, in an inert document so that the images aren't fetched. The median of 31 samples, in 3 runs:widthandheight, already written by core[width]and[height]Specs in
full_app_hydration_spec.ts:this.placeholdercondition.A spec in
event_replay_spec.tschecks that aloadevent that fired before hydration is replayed, under domino, which doesn't load images. It passes with and without this change.No spec covers the placeholder image that already loaded, because setting
completeandnaturalWidthunder domino also runs the dev-mode distortion check, which needs layout.Test targets for common and platform-server pass, including the Chromium and Firefox suites for common. I couldn't run
image-directive:webdriver_testsin my environment. That target fails the same way without this change.Risks
It affects apps that use
NgOptimizedImage.When nothing else writes the attributes of an image that loaded before hydration,
loadno longer fires a second time. That's the case for an image with no staticsizesattribute (measured in Chrome). A(load)handler on that image then runs once, through event replay, which is on by default. Without event replay, it doesn't run. Listeners added imperatively, such asfromEventorrenderer.listen, aren't covered by event replay and no longer receive that second event.An image without a placeholder that failed before hydration no longer fires
errora second time, and the browser doesn't try to load it again during hydration.(error)handlers use the same event replay path as(load), but no spec covers it.This change doesn't address the
sizesvalue that changes twice on lazy images. #71873 describes that case.