
Angular 21 の zoneless 化 - Zone.js を外す変更検知の新標準と移行チェック
Angular の「画面をいつ再描画するか」は、長年 Zone.js という仕組みが握ってきました。クリックや setTimeout、fetch の完了——こうした非同期イベントを Zone.js が裏で検知し、Angular に「変更検知を回せ」と知らせる。便利な一方で、バンドルサイズ・余計な再描画・デバッグの不透明さという代償もありました。
Angular 21 では、この Zone.js を外す「zoneless」変更検知が標準になりました。新規アプリでは Zone.js が既定で同梱されなくなり、zoneless が既定路線になります。この記事では、なぜ外すのか・Zone.js なしで何が再描画を起こすのか・どう移行するかを、React や Vue を触る人にも分かるように整理します。
Zone.js とは何だったのか
Zone.js は、ブラウザの非同期 API(setTimeout・addEventListener・Promise・XHR など)をモンキーパッチでフックし、「非同期処理が終わったら Angular に通知する」ことで変更検知を起動していました。開発者が何も書かなくても、イベントのたびに Angular が「どこか変わったかも」と画面を見直してくれる——この自動性が Angular の書き味を支えていました。
ただし代償があります。
- バンドルサイズ: Zone.js 自体の読み込みコスト
- 過剰な変更検知: 関係ない非同期イベントでもアプリ全体の検知が走りがち
- 不透明さ: 「なぜ今再描画されたのか」が追いにくい
近年の Angular は Signals(値の変化を細粒度で追うリアクティブな仕組み)を導入し、「変わった場所を Angular が正確に知る」方向へ進んできました。Signals があれば、Zone.js の「とりあえず全部見直す」方式は不要になります。これが zoneless の前提です。「自動リアクティビティを言語/フレームワーク側で精緻にする」流れは React Compiler とも通じます。
Zone.js なしで何が変更検知を起こすのか
zoneless モードでは、Angular は明示的な通知を受けたときだけ変更検知を回します。主なトリガーは次のとおりです(このほかにビューの削除やレンダーフックの登録なども含まれます)。
| トリガー | いつ起こるか |
|---|---|
| Signals | テンプレートで読んでいる signal を更新したとき |
ChangeDetectorRef.markForCheck() | 明示呼び出し。AsyncPipe が内部で自動的に呼ぶ |
ComponentRef.setInput() | 入力(Input)を直接更新したとき |
| イベントリスナ | ホスト/テンプレートにバインドしたリスナのコールバック |
| dirty なビューのアタッチ | dirty 印の付いたビューを接続したとき |
中心になるのが Signals です。「テンプレートが読んでいる signal を更新したら再描画」という、原因と結果がはっきりした関係になります。Zone.js 時代の「非同期が起きたから一応全体を見直す」と比べ、何が再描画を引き起こすかが明示的になるのが zoneless の本質です。
有効化と Zone.js の除去
有効化
Angular 21 では新規アプリが既定で zoneless です。Angular v20 など既存アプリで手動で有効にする場合は、ブートストラップで専用のプロバイダを渡します。
import { provideZonelessChangeDetection } from '@angular/core';
bootstrapApplication(App, {
providers: [
provideZonelessChangeDetection(),
],
});Zone.js を外してバンドルを減らす
zoneless にしたら、Zone.js 自体を取り除いてバンドルサイズを削れます。
angular.jsonのpolyfillsからzone.jsとzone.js/testingを削除(buildとtestの両方)polyfills.tsを使っている場合はimport 'zone.js'を削除npm uninstall zone.js
既存 Angular 案件の移行チェックリスト
OnPushを基本に: コンポーネントはできるだけChangeDetectionStrategy.OnPushを使う。zoneless と相性がよいNgZone依存を洗い出す:NgZone.onMicrotaskEmpty/onUnstable/onStableは zoneless では一切 emit しません。これらに依存した処理は作り直しが必要- 手動で再描画していた箇所:
setTimeoutや外部コールバックの中で状態を変えている場所は、signal 経由かmarkForCheck()で明示的に通知する形へ - 段階移行: いきなり全体を切り替えず、
OnPush化と signal 化を進めてから zoneless を有効にすると安全
WARNING
「zoneless にしたら一部の画面が更新されなくなった」の典型原因は、Zone.js の自動検知に暗黙的に頼っていた箇所です。外部ライブラリのコールバックや生の setTimeout の中で値を変えているのに、signal も markForCheck() も使っていない——というパターン。zoneless では「変えたら通知する」を明示しないと再描画されません。移行時はここを重点的に洗ってください。
React / Vue 勢への補足
変更検知モデルの違いで戸惑いがちなので、対比しておきます。
- React:
setState/ フックで状態を更新すると再レンダー。明示的な更新がトリガー - Vue: リアクティブな値の変化を追って再描画
- Angular(zoneless): signal の更新やイベントなど明示的な通知で変更検知
つまり Angular も、Zone.js の「魔法のような全体検知」から、React/Vue に近い「変えたところを起点に描く」モデルへ寄ってきた、と捉えると分かりやすいです。フレームワークをまたいで「細粒度リアクティビティ」へ収束している流れの一つです。Svelte/SvelteKit のコンパイル時リアクティビティとも、思想は同じ方向を向いています。
まとめ
- Angular 21 は zoneless 変更検知が標準。新規アプリは Zone.js が既定で同梱されなくなった
- Zone.js は非同期 API をフックして自動で変更検知を起動していたが、バンドル・過剰検知・不透明さが代償だった
- zoneless では Signals・
markForCheck()・setInput()・イベント・dirty ビューという明示的トリガーだけが再描画を起こす。主役は Signals - 有効化は
provideZonelessChangeDetection()(v20 等)。angular.jsonの polyfills からzone.jsを外しnpm uninstallでバンドル削減 - 移行は
OnPush化と signal 化を進め、NgZone.onStable等への依存を排除してから。暗黙の自動検知頼りが事故の元 - 変更検知モデルとしては React/Vue に近い「明示的な通知で描く」方向へ収束した
Zone.js は Angular の自動性を長く支えた立役者ですが、Signals の登場で役目を終えつつあります。zoneless は単なる軽量化ではなく、「なぜ再描画されたのかが分かる」という透明性への移行です。