Angular 21 の zoneless 化 - Zone.js を外す変更検知の新標準と移行チェック

Angular 21 の zoneless 化 - Zone.js を外す変更検知の新標準と移行チェック

作成日:
更新日:

Angular の「画面をいつ再描画するか」は、長年 Zone.js という仕組みが握ってきました。クリックや setTimeoutfetch の完了——こうした非同期イベントを Zone.js が裏で検知し、Angular に「変更検知を回せ」と知らせる。便利な一方で、バンドルサイズ・余計な再描画・デバッグの不透明さという代償もありました。

Angular 21 では、この Zone.js を外す「zoneless」変更検知が標準になりました。新規アプリでは Zone.js が既定で同梱されなくなり、zoneless が既定路線になります。この記事では、なぜ外すのか・Zone.js なしで何が再描画を起こすのか・どう移行するかを、React や Vue を触る人にも分かるように整理します。

Zone.js とは何だったのか

Zone.js は、ブラウザの非同期 API(setTimeoutaddEventListenerPromise・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 など既存アプリで手動で有効にする場合は、ブートストラップで専用のプロバイダを渡します。

zoneless を有効化(v20 など)
import { provideZonelessChangeDetection } from '@angular/core';
 
bootstrapApplication(App, {
  providers: [
    provideZonelessChangeDetection(),
  ],
});

Zone.js を外してバンドルを減らす

zoneless にしたら、Zone.js 自体を取り除いてバンドルサイズを削れます。

  1. angular.jsonpolyfills から zone.jszone.js/testing を削除buildtest の両方)
  2. polyfills.ts を使っている場合は import 'zone.js' を削除
  3. 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 は単なる軽量化ではなく、「なぜ再描画されたのかが分かる」という透明性への移行です。

参考リンク