Skip to content

Hydration writes every static attribute and binding again, even when the server rendered the same value #71971

Open

Description

@jsaguet

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

During hydration, Angular reuses the DOM nodes that the server rendered, but it still writes their values again:

  • setupStaticAttributes() writes every static attribute, the static class, and the static style of each reused element.
  • On the first update after hydration, attribute, property, class, and style bindings write their values, because they have no previous value to compare with.

The values are the ones the server rendered, so the DOM ends up the same. The writes still cost time, and a class or style write makes the browser recalculate styles.

#71874 stops these writes only for attributes that make the browser load a resource again: src on iframe, embed, video, and audio, and data on object. This issue tracks the cost of all the other writes.

On a table with 300 rows of static classes, ARIA attributes, and SVG icons, hydration makes 12,961 attribute writes, and 12,871 of them keep the same value. I measured an experimental patch that skips a static attribute, class, or style when the DOM already has its value:

Table, 300 rows Today Experimental patch
Attribute writes during hydration 12,961 365
Hydration time 22.3 ms 10.0 ms (−55%)
Hydration time, 4× CPU throttling 162.0 ms 61.6 ms (−62%)
Elements restyled after hydration 1,247 45

Hydration time runs from bootstrapApplication() to ApplicationRef.whenStable(), in headless Chrome 156 with the cache enabled. Each value is the median of 7 page loads. #62672 reported the same writes, and the discussion there put the visible effect down to DevTools disabling the cache. The cost measured here is main-thread time, not network.

The experimental patch isn't a proposal as it stands, because of class and style. An element can combine a static class with a binding or a directive, as in <div class="card" [class.active]="isActive">. The server then renders class="card active", which differs from the static card, so core writes card and the binding adds active back. A comparison doesn't remove those writes, and skipping the static value altogether breaks other cases (see #71200).

On the table, class accounts for 3,915 of the writes. Most of the others are static role, title, aria-*, width, and height attributes, which don't have that problem.

Please provide a link to a minimal reproduction of the bug

https://github.com/jsaguet/ngimg-hydration-repro

The experimental patch is the core variant in bench/patch.mjs at commit d6b35a8. The measurements above are in results/timings.txt and results/dom-writes.txt at the same commit. To run them, check out d6b35a8, then run npm ci, npm run build:variants, and npm run bench.

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?

Related: #52477, #52478, #71200, and #71874.

Activity

  1. added this to the needsTriage milestone on Nov 11, 2026
  2. added this to the needsTriage milestone on Nov 11, 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: serverIssues related to server-side rendering

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions