Problem
Angular 21 defaults to zoneless change detection. Code that relies on NgZone events like onMicrotaskEmpty and onStable will silently break — these observables never emit in zoneless mode.
We already hit this in the Carousel component , fixed in #14028 : buttons and indicators were missing because NgZone.onMicrotaskEmpty never fired.
The same class of bug exists in 27 other files across the project.
High Risk: onMicrotaskEmpty / onStable subscribers (17 files)
These will break in zoneless mode — the subscriptions never fire, causing missing UI, broken initialization, or stale state.
#
File
Usage
Line(s)
Assignee
PR
1
libs/cdk/utils/directives/initial-focus/initial-focus.directive.ts
onMicrotaskEmpty.pipe(take(1))
:74
Inna
#14044
2
libs/cdk/utils/directives/disabled/fdk-disabled-provider.service.ts
firstValueFrom(ngZone.onStable)
:70
Maria
3
libs/cdk/utils/directives/readonly/fdk-readonly-provider.service.ts
firstValueFrom(ngZone.onStable)
:75
Maria
4
libs/core/tabs/tab-list.component.ts
onStable + onMicrotaskEmpty
:379, :384
Deno
#14042
5
libs/core/tabs/tab-panel/tab-panel.component.ts
onStable.pipe(first())
:160
Deno
#14042
6
libs/core/form/form-item/form-item.component.ts
onStable.pipe(first())
:63
Inna
#14045
7
libs/core/table/table.component.ts
onStable.pipe(first())
:141
Mike
8
libs/core/timeline/components/timeline-node-body/timeline-node-body.component.ts
onStable.pipe(first())
:36
Maria
9
libs/core/menu/directives/segmented-button/segmented-button-option.directive.ts
delayWhen(() => ngZone.onStable)
:58
Deno
#14043
10
libs/core/user-menu/components/user-menu-list-item.component.ts
onStable.pipe(startWith(isStable))
:306
Inna
#14046
11
libs/btp/tool-header/src/components/tool-header/tool-header.component.ts
onStable (delayWhen + first())
:193, :250
Maria
12
libs/btp/navigation/components/navigation-start/navigation-content-start.component.ts
onStable.pipe(startWith(isStable))
:139
Deno
#14049
13
libs/btp/navigation/components/navigation-item/navigation-list-item.component.ts
onStable.pipe(startWith(isStable))
:963
Inna
#14047
14
libs/platform/icon-tab-bar/components/icon-tab-bar-base.class.ts
onMicrotaskEmpty.pipe(take(1))
:299
Deno
#14051
15
libs/platform/icon-tab-bar/components/icon-tab-bar-process-type/icon-tab-bar-process-type.component.ts
onMicrotaskEmpty.pipe(take(1))
:107
Deno
#14051
16
libs/platform/table/table.component.ts
onMicrotaskEmpty.pipe(take(1))
:2391
Mike
17
libs/docs/platform/form-generator/examples/platform-form-generator-inline-help-example.component.ts
onStable.pipe(first())
:102
Inna
#14048
Low Risk: runOutsideAngular / run (12 files)
These won't break (they're effectively no-ops in zoneless), but they are dead code that should be cleaned up:
#
File
Usage
Line(s)
Assignee
PR
1
libs/cdk/utils/directives/auto-complete/auto-complete.directive.ts
runOutsideAngular
:78
Maria
#14081
2
libs/cdk/utils/directives/overflow-list/overflow-list.directive.ts
run()
:115, :122
Maria
#14079
3
libs/btp/splitter/splitter-resizer/splitter-resizer.component.ts
runOutsideAngular + run
:188, :192, :203
Inna
#14052
4
libs/btp/tool-header/src/components/tool-header/tool-header.component.ts
runOutsideAngular
:275
Maria
#14076
5
libs/core/segmented-button/segmented-button.component.ts
run()
:233
Deno
#14053
6
libs/platform/form/auto-complete/auto-complete.directive.ts
runOutsideAngular
:86
Inna
#14054
7
libs/platform/table-helpers/directives/table-header-resizer.directive.ts
runOutsideAngular
:59
Mike
8
libs/platform/table-helpers/directives/table-cell-resizable.directive.ts
runOutsideAngular
:72
Mike
9
libs/platform/table-helpers/directives/table-scrollable.directive.ts
runOutsideAngular
:53, :198
Mike
10
libs/platform/table/components/table-column-resizer/table-column-resizer.component.ts
runOutsideAngular
:58, :102
Mike
11
libs/platform/table/table.component.ts
runOutsideAngular
:2345
Mike
12
libs/platform/table/components/table-row/table-row.component.ts
runOutsideAngular
:250
Mike
Test file to update
libs/platform/icon-tab-bar/components/icon-tab-bar-process-type/icon-tab-bar-process-type.component.spec.ts — mocks NgZone, needs updating alongside its component — Deno (same as component Code formatting using Prettier JS #15 )
Suggested migration patterns
Based on the approach used in #14028 (carousel fix):
Current pattern
Replacement
onMicrotaskEmpty.pipe(take(1)).subscribe(fn)
queueMicrotask(fn)
onStable.pipe(first()).subscribe(fn)
afterNextRender(fn) or queueMicrotask(fn)
firstValueFrom(ngZone.onStable).then(fn)
queueMicrotask(fn) or Promise.resolve().then(fn)
delayWhen(() => ngZone.onStable)
delay(0) or restructure with signals
runOutsideAngular(() => ...)
Remove wrapper, call directly
ngZone.run(() => ...)
Remove wrapper, call directly
After each migration, remove the NgZone import and injection.
Summary per developer
Developer
High Risk
Low Risk
Total
Mike
2 (core/table, platform/table)
6 (all table-related)
8
Maria
3 (disabled-provider, readonly-provider, timeline, tool-header)
3 (overflow-list, tool-header, auto-complete)
6
Inna
4 (initial-focus, form-item, user-menu, navigation-list-item, form-generator)
2 (splitter-resizer, auto-complete)
6
Deno
5 (tab-list, tab-panel, segmented-button-option, navigation-start, icon-tab-bar-process)
1 (segmented-button) + test file
6
Priority
The 17 high-risk files should be addressed first — they will cause the same silent failures as the carousel bug (missing UI elements, broken initialization).
References
Related issues
This is the parent tracking issue for Angular 21 zoneless migration. The following sub-issues cover additional areas that need attention:
Suggested order of work
Remove NgZone usage across the project for Angular 21 zoneless compatibility #14035 (this issue) — Remove all NgZone usages first (high-risk items are blocking)
Audit setTimeout/setInterval usage for zoneless compatibility #14037 — Audit setTimeout/setInterval call sites
Audit ChangeDetectionStrategy.Default components for zoneless compatibility #14039 — Migrate Default strategy components to OnPush
Remove zone.js polyfill and configure zoneless bootstrap for Angular 21 #14038 — Remove zone.js and enable zoneless bootstrap (only after 1-3 are done)
Problem
Angular 21 defaults to zoneless change detection. Code that relies on
NgZoneevents likeonMicrotaskEmptyandonStablewill silently break — these observables never emit in zoneless mode.We already hit this in the Carousel component, fixed in #14028: buttons and indicators were missing because
NgZone.onMicrotaskEmptynever fired.The same class of bug exists in 27 other files across the project.
High Risk:
onMicrotaskEmpty/onStablesubscribers (17 files)These will break in zoneless mode — the subscriptions never fire, causing missing UI, broken initialization, or stale state.
libs/cdk/utils/directives/initial-focus/initial-focus.directive.tsonMicrotaskEmpty.pipe(take(1))libs/cdk/utils/directives/disabled/fdk-disabled-provider.service.tsfirstValueFrom(ngZone.onStable)libs/cdk/utils/directives/readonly/fdk-readonly-provider.service.tsfirstValueFrom(ngZone.onStable)libs/core/tabs/tab-list.component.tsonStable+onMicrotaskEmptylibs/core/tabs/tab-panel/tab-panel.component.tsonStable.pipe(first())libs/core/form/form-item/form-item.component.tsonStable.pipe(first())libs/core/table/table.component.tsonStable.pipe(first())libs/core/timeline/components/timeline-node-body/timeline-node-body.component.tsonStable.pipe(first())libs/core/menu/directives/segmented-button/segmented-button-option.directive.tsdelayWhen(() => ngZone.onStable)libs/core/user-menu/components/user-menu-list-item.component.tsonStable.pipe(startWith(isStable))libs/btp/tool-header/src/components/tool-header/tool-header.component.tsonStable(delayWhen + first())libs/btp/navigation/components/navigation-start/navigation-content-start.component.tsonStable.pipe(startWith(isStable))libs/btp/navigation/components/navigation-item/navigation-list-item.component.tsonStable.pipe(startWith(isStable))libs/platform/icon-tab-bar/components/icon-tab-bar-base.class.tsonMicrotaskEmpty.pipe(take(1))libs/platform/icon-tab-bar/components/icon-tab-bar-process-type/icon-tab-bar-process-type.component.tsonMicrotaskEmpty.pipe(take(1))libs/platform/table/table.component.tsonMicrotaskEmpty.pipe(take(1))libs/docs/platform/form-generator/examples/platform-form-generator-inline-help-example.component.tsonStable.pipe(first())Low Risk:
runOutsideAngular/run(12 files)These won't break (they're effectively no-ops in zoneless), but they are dead code that should be cleaned up:
libs/cdk/utils/directives/auto-complete/auto-complete.directive.tsrunOutsideAngularlibs/cdk/utils/directives/overflow-list/overflow-list.directive.tsrun()libs/btp/splitter/splitter-resizer/splitter-resizer.component.tsrunOutsideAngular+runlibs/btp/tool-header/src/components/tool-header/tool-header.component.tsrunOutsideAngularlibs/core/segmented-button/segmented-button.component.tsrun()libs/platform/form/auto-complete/auto-complete.directive.tsrunOutsideAngularlibs/platform/table-helpers/directives/table-header-resizer.directive.tsrunOutsideAngularlibs/platform/table-helpers/directives/table-cell-resizable.directive.tsrunOutsideAngularlibs/platform/table-helpers/directives/table-scrollable.directive.tsrunOutsideAngularlibs/platform/table/components/table-column-resizer/table-column-resizer.component.tsrunOutsideAngularlibs/platform/table/table.component.tsrunOutsideAngularlibs/platform/table/components/table-row/table-row.component.tsrunOutsideAngularTest file to update
libs/platform/icon-tab-bar/components/icon-tab-bar-process-type/icon-tab-bar-process-type.component.spec.ts— mocksNgZone, needs updating alongside its component — Deno (same as component Code formatting using Prettier JS #15)Suggested migration patterns
Based on the approach used in #14028 (carousel fix):
onMicrotaskEmpty.pipe(take(1)).subscribe(fn)queueMicrotask(fn)onStable.pipe(first()).subscribe(fn)afterNextRender(fn)orqueueMicrotask(fn)firstValueFrom(ngZone.onStable).then(fn)queueMicrotask(fn)orPromise.resolve().then(fn)delayWhen(() => ngZone.onStable)delay(0)or restructure with signalsrunOutsideAngular(() => ...)ngZone.run(() => ...)After each migration, remove the
NgZoneimport and injection.Summary per developer
Priority
The 17 high-risk files should be addressed first — they will cause the same silent failures as the carousel bug (missing UI elements, broken initialization).
References
Related issues
This is the parent tracking issue for Angular 21 zoneless migration. The following sub-issues cover additional areas that need attention:
setTimeout/setIntervalusage (85 occurrences across 50 files — callbacks that mutate template-bound state won't trigger CD without zone.js)zone.jspolyfill and configure zoneless bootstrap (polyfills.ts, project.json, package.json, StackBlitz templates all still reference zone.js)ChangeDetectionStrategy.Defaultcomponents (15 files with explicit Default strategy that rely on zone-triggered CD)Suggested order of work
NgZoneusages first (high-risk items are blocking)setTimeout/setIntervalcall sitesDefaultstrategy components toOnPushzone.jsand enable zoneless bootstrap (only after 1-3 are done)