Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
common
Is this a regression?
No
Description
During hydration, NgOptimizedImage makes the browser select the source of each image again. There are two separate causes.
The directive writes its attributes again. setHostAttributes() calls renderer.setAttribute() for src, srcset, sizes, and six other attributes. The server rendered the same values. A draft pull request skips the writes when the element already has the value.
The value of sizes changes twice on a lazy image. The directive prepends auto to sizes on lazy images, so the value that it computes differs from the template. This happens only on a lazy image with a static sizes attribute:
server HTML sizes="auto, 50vw"
hydration, core sizes="50vw" setupStaticAttributes() writes the template value
hydration, directive sizes="auto, 50vw" setHostAttributes() prepends auto again
The directive computes the same value on the server and in the browser, but core's write runs before the directive exists. Each of these two writes really changes the value, so checking for an unchanged value doesn't remove them.
The browser also fires load again on images that loaded before hydration. Event replay delivers the original load event too, so a (load) handler runs twice on those images.
On a page with 200 images, where hydration starts after the window load event, hydration takes 35.0 ms today and 27.7 ms with the draft pull request. When the same page uses [width], [height], and [sizes] bindings, which core doesn't write to the DOM, hydration takes 9.9 ms with the draft pull request.
Please provide a link to a minimal reproduction of the bug
https://github.com/jsaguet/ngimg-hydration-repro
To see the (load) handler run twice, run npm ci, npm run build:variants, and npm run serve:stock, and open http://localhost:4000/?bootAfterLoad. A badge under each image counts the calls. The README has the measurements.
Please provide the exception or error you saw
No exception.
Please provide the environment you discovered this bug in (run ng version)
Angular CLI : 22.2.6
Angular : 22.2.5
Node.js : 26.11.1
Package Manager : npm 11.20.0
Chrome : 156
Anything else?
Until the sizes case is fixed, bind the input, for example [sizes]="'50vw'", so that core doesn't write it to the DOM.
I don't have a proposal for the sizes case that I'm confident about. These are the options I looked at:
- Don't prepend
autowhen the template already has it, so that a developer can writesizes="auto, 50vw". This is one condition in the directive. It helps only the templates that change. - Make the check in core treat
auto, Xin the DOM as equal toXin the template. This covers existing templates, but core would then know about something that the directive does. - Let a directive declare the host attributes that it writes itself, so that core doesn't write them. This covers existing templates, and it adds an internal mechanism for a single attribute.
Which direction would you prefer, if this case is worth fixing?
Activity