はじめに
「AngularでZoneless化が始まってる」という話をよく見かけるようになりました。
聞いたことはあっても、そもそも今まで動いていた Zone.js が何をしていたのか、Zonelessになると何がどう変わるのか、自分の中で整理できていなかったので、改めて調べてみました。
Zone.jsとは何か
Zone.jsは、ブラウザの非同期API(setTimeout、Promise、addEventListenerなど)にパッチを当て、非同期処理が発生・完了したタイミングを検知できるようにするライブラリです。Angularはこれを長らく変更検知(Change Detection)の仕組みとして利用してきました。
つまり「アプリのどこかで非同期処理が起きたら、そのタイミングでAngularに知らせる」という役割を担っていたのがZone.jsです。
Zone.jsがやっていた変更検知の仕組み
Zone.jsが管理する非同期タスクやイベント(setTimeout、Promiseの解決、クリックイベントなど)の処理が完了すると、Angularは「アプリのどこかで状態が変わったかもしれない」と判断し、アプリケーション単位で変更検知を開始します。デフォルトの変更検知戦略を使うビューは広く検査されますが、OnPushで変更なしと判断されたサブツリーなどはスキップされます。
これにより、開発者は変更検知を意識せず「データを更新すれば勝手に画面が更新される」という体験ができていました。ただし、この仕組みは「何かあったら全部チェックする」という粗いトリガーのため、以下のような代償もありました。
- 非同期処理が起きるたびにツリー全体を再チェックするため、無駄な変更検知サイクルが発生する
- スタックトレースがZone.js経由のフレームで埋まり、デバッグしづらくなる
-
async/awaitはJavaScriptの言語構文であり、Zone.jsが直接パッチできる対象ではないため、ビルド時の変換や実行環境との組み合わせによってはタスク追跡との互換性を考慮する必要がある - パッチ処理自体がバンドルサイズや起動時のオーバーヘッドになる
なお、Zone.js自体は現在、新機能の追加を停止しており、ネイティブなプラットフォームAPIへの新規パッチも行われていません。低優先度のバグ修正も受け付けなくなっていますが、AngularがZone.jsを直接利用している部分に関わるクリティカルな修正は引き続き受け付けられる、という位置づけになっています。廃止ではなく「サポートはするが新規開発の対象外」という状態です。
Zonelessとは何か
Zonelessは、Zone.jsによる「何か起きたら全部チェックする」という推測ベースの変更検知をやめ、状態が変わったことをAngular自身に明示的に伝える仕組みに置き換えるアプローチです。
具体的には、以下のようなものが変更検知のトリガーになります。
- テンプレートで読んでいるSignalの値が更新される
-
ComponentRef.setInput()によって入力プロパティが変更される - テンプレートに束縛されたイベント(
(click)など)が発火する -
ChangeDetectorRef.markForCheck()が呼ばれる - ダーティーとしてマークされたビューがアタッチされる、またはビューが削除される
「Zone.jsが非同期処理を包括的に監視して変更検知を始める方式」から、「Angularが把握できる通知を起点に、必要なビューを変更検知の対象としてスケジュールする方式」への転換、というのがZonelessの考え方です。
具体的にどう置き換わるか
Signalsがそのままトリガーになる
テンプレートで参照しているSignalの値が変わると、そのビューが変更対象としてマークされ、変更検知がスケジュールされます。書き方自体はZone.js時代とほとんど変わりません。
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1); // これがそのままChange Detectionのトリガーになる
}
}
ObservableはasyncパイプかtoSignal()経由でつなぐ
RxJSのObservableをそのまま使う場合は、asyncパイプがsubscribeして値を受け取り、内部で変更検知を要求する形になります。Observableを一度Signalに変換してから使うtoSignal()という書き方もあります。
テンプレートのイベントバインディングはそのまま使える
(click)や(input)のようにテンプレート上で宣言されたイベントは、Angular自身がその発生元を把握しているため、ハンドラ実行後に自動でチェックが走ります。ここはZone.js時代と体感差がありません。
サードパーティのコールバックやraw setTimeoutは自分で橋渡しが必要
ここが一番の注意点です。Zone.js時代は「パッチ済みのAPIなら何でも拾ってくれる」万能さがありましたが、Zonelessではパッチが存在しないため、通常のフィールドを書き換えるだけでは変更検知がスケジュールされません。Signalの更新、markForCheck()の呼び出し、ComponentRef.setInput()、AsyncPipe経由での受け取りなど、Angularが認識できる通知経路を介す必要があります。
// 通常のフィールドを書き換えるだけでは、
// Zoneless環境で変更検知がスケジュールされない
setTimeout(() => {
this.someRawVariable = 'updated';
}, 1000);
// テンプレートで参照しているSignalを更新すると、
// Angularへ変更が通知される
setTimeout(() => {
this.someSignal.set('updated');
}, 1000);
古いライブラリで手動で変更検知を呼び出していたようなものについても、ラッパーやSignal・markForCheck()での橋渡しが必要になる場合があります。
SSRの「安定判定」も仕組みが変わる
これまでのSSRでは、Zone.jsを利用するNgZoneの通知をもとに、Angularが「アプリの描画が落ち着いた(stable)」タイミングを判定していましたが、Zoneless下ではPendingTasksというサービスに、シリアライズを待つべき非同期処理があることを明示的に伝える形になります。
有効化する方法
provideZonelessChangeDetection()というproviderを追加し、polyfillsからzone.jsを取り除くことでZonelessに切り替えられます。
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
新規プロジェクトを作成する場合は、CLIに--zonelessオプションを付けることで、Zone.jsを使わない状態のアプリを最初から生成できます。
バージョンの経緯
- v18で、Zonelessが実験的(Developer Preview)に導入された
- v20.2で、Zoneless変更検知が安定版(Stable)になった
- v21(2025年11月)から、新規プロジェクトではZonelessがデフォルトになり、Zone.jsは標準では同梱されなくなった
まとめ
- Zone.jsは、ブラウザの非同期APIにパッチを当て、非同期処理の発生を検知するライブラリ
- Angularはこれを利用し、Zone.jsから非同期処理の完了を通知され、比較的広い範囲を対象とする変更検知を開始していた
- 代償として、無駄な変更検知サイクル、デバッグのしづらさ、バンドルサイズの増加などがあった
- Zonelessは、Signalの更新・イベントバインディング・
markForCheck()・asyncパイプなど、明示的な通知を変更検知のトリガーにする仕組み - raw
setTimeoutやサードパーティのコールバック内で通常のフィールドを書き換えるだけでは変更検知がスケジュールされないため、Signalの更新やmarkForCheck()など、Angularが認識できる通知経路を介する必要がある点に注意が必要 - v18で実験的導入、v20.2で安定版、v21から新規プロジェクトのデフォルトという流れで進んできた
Zone.jsが「何でも拾ってくれる自動検知」だったのに対し、Zonelessは「Signal・イベントバインディング・markForCheck・asyncパイプという明示的な経路」に置き換わった、という整理をすると自分の中でだいぶすっきりしました。
参考になったら いいね や ストック をお願いします!
同じような疑問を持ったことがある方のコメントもお待ちしています。
参考
関連リンク
技術ブログでも学びや検証内容をまとめています。