Repository navigation
Description
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 staticclass, and the staticstyleof 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
Activity