Skip to content

NgOptimizedImage makes the browser select each image source again during hydration #71873

Open

Description

@jsaguet

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 auto when the template already has it, so that a developer can write sizes="auto, 50vw". This is one condition in the directive. It helps only the templates that change.
  • Make the check in core treat auto, X in the DOM as equal to X in 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

  1. added this to the needsTriage milestone on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: commonIssues related to APIs in the @angular/common package

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions