Skip to content

Commit 9afd90c

Browse files
JeanMecheAndrewKushnir
authored andcommitted
refactor(core): Add a warning when ApplicationRef.isStable doesn't emit true (#50295)
Hydration requires a stable App to run some logic. With this warning developers will be informed about potential issues encountered when running an unstable app. Fixes #50285 PR Close #50295
1 parent bb48756 commit 9afd90c

4 files changed

Lines changed: 95 additions & 2 deletions

File tree

aio/content/errors/NG0506.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@name NgZone remains unstable
2+
@category runtime
3+
@shortDescription NgZone remains unstable after a long period of time
4+
5+
@description
6+
This warning occurs when hydration is enabled on the client but the NgZone remains unstable for a long period of time.
7+
8+
The {@link ApplicationRef#isStable} API uses NgZone to report when an application becomes `stable` and `unstable`. An application is considered stable when there are no pending microtasks or macrotasks.
9+
10+
Angular Hydration relies on a signal from Zone.js when it becomes stable inside an application:
11+
12+
* during the server-side rendering (SSR) to start the serialization process
13+
* in a browser this signal is used to start the post-hydration cleanup to remove DOM nodes that remained unclaimed
14+
15+
This warning is displayed when the `ApplicationRef.isStable()` doesn't emit `true` within 10 seconds. If this is intentional and your application becomes stable later, you can ignore this warning.
16+
17+
**Macrotasks**
18+
19+
Macrotasks include functions like `setInterval`, `setTimeout`, `requestAnimationFrame` etc.
20+
If one of these functions is called in the initialization phase of the app or the bootstrapped components, the application will remain unstable.
21+
22+
```typescript
23+
@Component({
24+
standalone: true,
25+
selector: 'app',
26+
template: ``,
27+
})
28+
class SimpleComponent {
29+
constructor() {
30+
setInterval(() => { ... }, 1000)
31+
32+
// or
33+
34+
setTimeout(() => { ... }, 10_000)
35+
}
36+
}
37+
```
38+
39+
If these functions need to be called in the initialization phase, invoking them outside the angular zone solves the issue.
40+
41+
```typescript
42+
class SimpleComponent {
43+
constructor() {
44+
inject(NgZone).runOutsideAngular(() => {
45+
setInterval(() => {}, 1000);
46+
})
47+
}
48+
}
49+
```
50+
51+
@debugging
52+
53+
Verify that you don't have any long standing microtask or macrotasks.

goldens/public-api/core/errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export const enum RuntimeErrorCode {
4949
// (undocumented)
5050
HYDRATION_NODE_MISMATCH = -500,
5151
// (undocumented)
52+
HYDRATION_STABLE_TIMEDOUT = -506,
53+
// (undocumented)
5254
IMPORT_PROVIDERS_FROM_STANDALONE = 800,
5355
// (undocumented)
5456
INJECTOR_ALREADY_DESTROYED = 205,

packages/core/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export const enum RuntimeErrorCode {
7575
UNSUPPORTED_PROJECTION_DOM_NODES = -503,
7676
INVALID_SKIP_HYDRATION_HOST = -504,
7777
MISSING_HYDRATION_ANNOTATIONS = -505,
78+
HYDRATION_STABLE_TIMEDOUT = -506,
7879

7980
// Signal Errors
8081
SIGNAL_WRITE_FROM_ILLEGAL_CONTEXT = 600,

packages/core/src/hydration/api.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {enableApplyRootElementTransformImpl} from '../render3/instructions/share
2222
import {enableLocateOrCreateContainerAnchorImpl} from '../render3/instructions/template';
2323
import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text';
2424
import {TransferState} from '../transfer_state';
25+
import {NgZone} from '../zone';
2526

2627
import {cleanupDehydratedViews} from './cleanup';
2728
import {IS_HYDRATION_DOM_REUSE_ENABLED, PRESERVE_HOST_CONTENT} from './tokens';
@@ -35,6 +36,12 @@ import {enableFindMatchingDehydratedViewImpl} from './views';
3536
*/
3637
let isHydrationSupportEnabled = false;
3738

39+
/**
40+
* Defines a period of time that Angular waits for the `ApplicationRef.isStable` to emit `true`.
41+
* If there was no event with the `true` value during this time, Angular reports a warning.
42+
*/
43+
const APPLICATION_IS_STABLE_TIMEOUT = 10_000;
44+
3845
/**
3946
* Brings the necessary hydration code in tree-shakable manner.
4047
* The code is only present when the `provideClientHydration` is
@@ -88,8 +95,24 @@ function printHydrationStats(injector: Injector) {
8895
* Returns a Promise that is resolved when an application becomes stable.
8996
*/
9097
function whenStable(
91-
appRef: ApplicationRef, pendingTasks: InitialRenderPendingTasks): Promise<unknown> {
98+
appRef: ApplicationRef, pendingTasks: InitialRenderPendingTasks,
99+
injector: Injector): Promise<unknown> {
92100
const isStablePromise = appRef.isStable.pipe(first((isStable: boolean) => isStable)).toPromise();
101+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
102+
const timeoutTime = APPLICATION_IS_STABLE_TIMEOUT;
103+
const console = injector.get(Console);
104+
const ngZone = injector.get(NgZone);
105+
106+
// The following call should not and does not prevent the app to become stable
107+
// We cannot use RxJS timer here because the app would remain unstable.
108+
// This also avoids an extra change detection cycle.
109+
const timeoutId = ngZone.runOutsideAngular(() => {
110+
return setTimeout(() => logWarningOnStableTimedout(timeoutTime, console), timeoutTime);
111+
});
112+
113+
isStablePromise.finally(() => clearTimeout(timeoutId));
114+
}
115+
93116
const pendingTasksPromise = pendingTasks.whenAllTasksComplete;
94117
return Promise.allSettled([isStablePromise, pendingTasksPromise]);
95118
}
@@ -166,7 +189,7 @@ export function withDomHydration(): EnvironmentProviders {
166189
const pendingTasks = inject(InitialRenderPendingTasks);
167190
const injector = inject(Injector);
168191
return () => {
169-
whenStable(appRef, pendingTasks).then(() => {
192+
whenStable(appRef, pendingTasks, injector).then(() => {
170193
// Wait until an app becomes stable and cleanup all views that
171194
// were not claimed during the application bootstrap process.
172195
// The timing is similar to when we start the serialization process
@@ -185,3 +208,17 @@ export function withDomHydration(): EnvironmentProviders {
185208
}
186209
]);
187210
}
211+
212+
/**
213+
*
214+
* @param time The time in ms until the stable timedout warning message is logged
215+
*/
216+
function logWarningOnStableTimedout(time: number, console: Console): void {
217+
const message =
218+
`Angular hydration expected the ApplicationRef.isStable() to emit \`true\`, but it ` +
219+
`didn't happen within ${
220+
time}ms. Angular hydration logic depends on the application becoming stable ` +
221+
`as a signal to complete hydration process.`;
222+
223+
console.warn(formatRuntimeError(RuntimeErrorCode.HYDRATION_STABLE_TIMEDOUT, message));
224+
}

0 commit comments

Comments
 (0)