8000 Bug: ssr + zoneless + defer + inputs fails to resolve input data on first render · Issue #61038 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content
8000

Bug: ssr + zoneless + defer + inputs fails to resolve input data on first render #61038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jsaguet opened this issue Apr 27, 2025 · 8 comments
Closed
Assignees
Labels
area: core Issues related to the framework runtime area: server Issues related to server-side rendering core: defer Issues related to @defer blocks.
Milestone

Comments

@jsaguet
Copy link
Contributor
jsaguet commented Apr 27, 2025

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

I don't know

Is this a regression?

Yes

Description

When using incremental hydration with zoneless change detection, the prerendered html (or returned from SSR) is incorrect.
Template is incomplete in the generated HTML for components inside @defer(hydrate never).

This behavior seems to be the same no matter the hydrate trigger.

When using zone change detection, the prerendering works fine.

Please provide a link to a minimal reproduction of the bug

https://github.com/jsaguet/ng-zoneless-hydration-prerender

Please provide the exception or error you saw

The generated html in index.html is incorrect:

<app-hydrated><p></p></app-hydrated>

when it should have been

<app-hydrated><p>Hydrate Never works!</p></app-hydrated>

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 19.0.5
Node: 22.13.0
Package Manager: npm 10.9.2
OS: win32 x64

Angular: 19.0.5
... animations, build, cli, common, compiler, compiler-cli, core
... platform-browser, platform-browser-dynamic, platform-server
... router, ssr

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1900.5
@angular-devkit/core         19.0.5
@angular-devkit/schematics   19.0.5
@schematics/angular          19.0.5
rxjs                         7.8.2
typescript                   5.6.3
zone.js                      0.15.0

Anything else?

The regression starts with Angular 19.0.5 and seems to have been fixed in 20.0.0-next.4

I'm not sure which commit in the 20.0.0-next.4 release fixes it but it would be great to have it backported to the release 19 even though zoneless is still experimental in this version.

@ngbot ngbot bot added this to the needsTriage milestone Apr 28, 2025
@thePunderWoman
Copy link
Contributor

I was able to see the same thing with the repro. I'll look further into it to see what's going on here.

@thePunderWoman
Copy link
Contributor

This actually is not just a pre-rendering issue. It also impacts SSR overall. Zoneless is currently experimental and we're still working on stabilizing it specifically with SSR cases. There's a callout in the Zoneless guide mentioning feature gaps with SSR around timing of serialization and it happening too early. This is the exact thing happening.

The interesting thing is that the incremental hydration behavior is working just fine and as intended. The issue is actually the signal input. Serialization seems to happen before that signal input task is resolved. The fix takes care of it with rendering after a macrotask. angular/angular-cli@6bd7b9b

Given that this is experimental still, I don't think we'd backport it, but it probably warrants more investigation as we need to ensure we have the right fix in place for this.

@thePunderWoman thePunderWoman changed the title Prerendering is broken when using incremental hydration with zoneless Bug: Signal input timing is not working across defer hydrate boundaries Apr 28, 2025
@thePunderWoman thePunderWoman changed the title Bug: Signal input timing is not working across defer hydrate boundaries Bug: ssr + zoneless + defer + inputs fails to resolve input data on first render Apr 28, 2025
@thePunderWoman
Copy link
Contributor

So it's actually unrelated to signal inputs too. Any type of input post 19.0.4 has this problem, but only on the very very first time the page is rendered. If you hard refresh the page after that, it renders fine. So there's something up with that first render post app-build, which doesn't appear to be on the framework side. We haven't been able to replicate this in a test environment at all either. I think this may be a CLI issue. I'll move this ticket over to that repo for further diagnosis.

@ngbot ngbot bot modified the milestone: needsTriage Apr 28, 2025
@thePunderWoman thePunderWoman transferred this issue from angular/angular Apr 28, 2025
@thePunderWoman thePunderWoman removed their assignment Apr 28, 2025
@jsaguet
Copy link
Contributor Author
jsaguet commented Apr 28, 2025

Thank you for the investigation @thePunderWoman

Thanks to the details you shared, I've managed to workaround the issue with the following code in the root component:

const endTask = this.pendingTasks.add();
setTimeout(() => endTask(), 0);

@alan-agius4
Copy link
Contributor
alan-agius4 commented Apr 29, 2025

I had a deeper look at this and I switched the code to directly call the "low-level" rendering APIs:

renderApplication(bootstrap, {
  url: '',
  document: '<app-root></app-root>',
})
  .then(console.log)
  .catch(console.error);

Even with version 19.0.4, the content still isn’t rendered correctly. Here's the resulting HTML:

<html><head></head><body><!--nghm--><app-root ng-version="19.0.4" ngh="0" ng-server-context="other"><!----><app-hydrated><p></p></app-hydrated><!--ngh=d0--></app-root><script id="ng-state" type="application/json">{"__nghData__":[{"t":{"0":"t0","1":"t1"},"c":{"0":[],"1":[{"i":"t0","r":1,"di":"d0","s":2}]}}],"__nghDeferData__":{"d0":{"p":null,"r":1,"s":2,"t":[]}}}</script></body></html>

I added some basic console.log statements and noticed from the debug logs that the application is being marked as stable prematurely:

add task id: 1
add task id: 2
remove task id: 1
number of tasks:1
remove task id: 0
number of tasks:0
add task id: 3
whenStable=true
whenStable=true
add task id: 4
remove task id: 2
number of tasks:1
remove task id: 3
number of tasks:0
add task id: 5
remove task id: 4
number of tasks:0

Looking into the PendingTasks class https://github.com/angular/angular/blob/main/packages/core/src/pending_tasks.ts#L54, there doesn't appear to be logic to handle scenarios where the app becomes stable between microtask creations. Previously, in NgZone, stabilization was deferred to a microtask

queueMicrotask(() => {
. However, this approach isn’t sufficient in zoneless mode, since we lose reliable tracking of pending micro and macro tasks.

While checking how pending task removal is handled for @defer blocks, I noticed the task is removed before the microtask completes

pendingTasks.remove(taskId);
However, moving the removal into a .finally() block seems to resolve this race condition:

return tDetails.loadingPromise.finally(() => {
  // Loading is completed, we no longer need the loading Promise
  // and the pending task should also be removed.
  tDetails.loadingPromise = null;
  pendingTasks.remove(taskId);
});

@alan-agius4 alan-agius4 transferred this issue from angular/angular-cli Apr 29, 2025
@thePunderWoman
Copy link
Contributor

I had a deeper look at this and I switched the code to directly call the "low-level" rendering APIs:

renderApplication(bootstrap, {
url: '',
document: '',
})
.then(console.log)
.catch(console.error);
Even with version 19.0.4, the content still isn’t rendered correctly. Here's the resulting HTML:

<script id="ng-state" type="application/json">{"__nghData__":[{"t":{"0":"t0","1":"t1"},"c":{"0":[],"1":[{"i":"t0","r":1,"di":"d0","s":2}]}}],"__nghDeferData__":{"d0":{"p":null,"r":1,"s":2,"t":[]}}}</script> I added some basic `console.log` statements and noticed from the debug logs that the application is being marked as stable prematurely:
add task id: 1
add task id: 2
remove task id: 1
number of tasks:1
remove task id: 0
number of tasks:0
add task id: 3
whenStable=true
whenStable=true
add task id: 4
remove task id: 2
number of tasks:1
remove task id: 3
number of tasks:0
add task id: 5
remove task id: 4
number of tasks:0

Looking into the PendingTasks class https://github.com/angular/angular/blob/main/packages/core/src/pending_tasks.ts#L54, there doesn't appear to be logic to handle scenarios where the app becomes stable between microtask creations. Previously, in NgZone, stabilization was deferred to a microtask

angular/packages/core/src/zone/ng_zone.ts

Line 566 in 6c8faaa

queueMicrotask(() => {
. However, this approach isn’t sufficient in zoneless mode, since we lose reliable tracking of pending micro and macro tasks.
While checking how pending task removal is handled for @defer blocks, I noticed the task is removed before the microtask completes

angular/packages/core/src/defer/triggering.ts

Line 250 in 2445946

pendingTasks.remove(taskId);
However, moving the removal into a .finally() block seems to resolve this race condition:
return tDetails.loadingPromise.finally(() => {
// Loading is completed, we no longer need the loading Promise
// and the pending task should also be removed.
tDetails.loadingPromise = null;
pendingTasks.remove(taskId);
});

Wow, I'm surprised you were able to replicate this in 19.0.4 when I was unable to. For me things worked in 19.0.4 and moving to 19.0.5 showed the reported behavior. So I'm curious what you did differently to see this. We've been unable to replicate in tests. So I'd be interested to see what you've done there.

@sod

This comment has been minimized.

alan-agius4 added a commit to alan-agius4/angular that referenced this issue Apr 29, 2025
Previously, the app was marked as stable prematurely. For more details, see angular#61038 (comment)

Closes: angular#61038
@pkozlowski-opensource pkozlowski-opensource added area: core Issues related to the framework runtime area: server Issues related to server-side rendering core: defer Issues related to @defer blocks. labels Apr 30, 2025
@ngbot ngbot bot modified the milestone: needsTriage Apr 30, 2025
alan-agius4 added a commit to alan-agius4/angular that referenced this issue Apr 30, 2025
Previously, the app was marked as stable prematurely. For more details, see angular#61038 (comment)

Closes: angular#61038
(cherry picked from commit de649c9)
@alan-agius4 alan-agius4 self-assigned this Apr 30, 2025
AndrewKushnir pushed a commit that referenced this issue Apr 30, 2025
Previously, the app was marked as stable prematurely. For more details, see #61038 (comment)

Closes: #61038
(cherry picked from commit de649c9)

PR Close #61056
AndrewKushnir pushed a commit that referenced this issue Apr 30, 2025
Previously, the app was marked as stable prematurely. For more details, see #61038 (comment)

Closes: #61038

PR Close #61040
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators May 31, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: core Issues related to the framework runtime area: server Issues related to server-side rendering core: defer Issues related to @defer blocks.
Projects
None yet
5 participants
0