Angular 19 Zoneless:Zone.jsなしのパフォーマンスと変更検知

Angular 19とAngular 20のZoneless変更検知により、Zone.jsを完全に削除し、バンドルサイズを33KB削減、レンダリング速度を30-40%向上させます。provideExperimentalZonelessChangeDetectionからprovideZonelessChangeDetectionへの移行パス、シグナルベースの反応性、既存アプリケーションの移行における落とし穴、SSR対応、パフォーマンスベンチマークを詳しく解説します。

シグナルとパフォーマンス最適化を示すAngular zoneless変更検知アーキテクチャ図

Angular zoneless変更検知は、スタンドアロンコンポーネント導入以来、フレームワークにおける最も重要なアーキテクチャの変革です。Zone.jsを完全に削除することで、Angularアプリケーションは約33KBの小さなバンドル、30〜40%削減された不要な変更検知サイクル、Zone特有のノイズのないクリーンなスタックトレースを実現します。

Zonelessのタイムライン

Angular 18でzonelessが実験的に導入されました。Angular 19ではprovideExperimentalZonelessChangeDetection()を使用した実験的APIが洗練されました。Angular 20ではprovideZonelessChangeDetection()として安定版に昇格しました。Angular 21では新規プロジェクトのデフォルトになります。

Zone.js変更検知の内部動作

zonelessを理解する前に、Zone.jsのメカニズムを明確に説明する必要があります。Zone.jsはすべての非同期ブラウザAPIをモンキーパッチします:setTimeoutsetIntervalPromise.thenaddEventListenerXMLHttpRequestなど、数十のAPIです。これらのAPIのいずれかが完了するたびに、Zone.jsはAngularに通知し、Angularはコンポーネントツリー全体で変更検知を実行します。

このアプローチには根本的な欠陥があります。Zone.jsはアプリケーションの状態が実際に変更されたかどうかを知ることができません。アニメーションタイミングのためだけに使用されるsetTimeoutでも、完全な変更検知サイクルをトリガーします。数百のコンポーネントを持つ大規模なアプリケーションでは、このオーバーヘッドは測定可能になります。

app.config.ts - 従来のZone.jsセットアップ(Angular 18-19)typescript
import { ApplicationConfig } from '@angular/core';
import { provideZoneChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    // Zone.jsは約130以上のブラウザAPIにパッチを当てる
    // すべての非同期コールバックが変更検知をトリガーする
  ]
};

イベント合体(Angular 14で導入)は、複数のイベントを単一の変更検知サイクルにバッチ処理することでオーバーヘッドを削減しますが、核心的な問題は残ります:変更検知は必要以上に頻繁に実行されます。

Angular 19と20でZoneless変更検知を有効にする

移行パスはAngularのバージョンによって異なります。Angular 19では実験的APIを使用し、Angular 20では安定版を提供します。

app.config.ts - Angular 19(実験的)typescript
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    // Zone.jsパッチングなし
  ]
};
app.config.ts - Angular 20+(安定版)typescript
import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),
  ]
};

プロバイダーを切り替えた後、angular.jsonのビルドとテストターゲットの両方でpolyfills配列からzone.jsを削除し、パッケージをアンインストールします。

bash
# angular.jsonのビルドとテストターゲットからzone.jsポリフィルを削除
# その後、パッケージをアンインストール
npm uninstall zone.js

バンドルサイズの削減は即座に現れます:Zone.jsは約33KB(gzip圧縮で10KB)のイーガーロードされたコードを占めています。

Zonelessモードで変更検知をトリガーするもの

すべての非同期操作をインターセプトするZone.jsがなければ、Angularは明示的な通知に依存します。フレームワークは次のいずれかの条件が発生すると変更検知をスケジュールします。

  • テンプレートで読み取られたシグナルがその値を更新する
  • ChangeDetectorRef.markForCheck()が呼び出される(AsyncPipeによって自動的に)
  • コンポーネント入力がComponentRef.setInput()を介して変更される
  • テンプレートまたはホストリスナーコールバックが実行される(click、inputなど)
  • 以前にダーティなビューがコンポーネントツリーにアタッチされる
counter.component.ts - シグナル駆動の変更検知typescript
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div class="counter">
      <button (click)="decrement()">-</button>
      <span>{{ count() }}</span>
      <button (click)="increment()">+</button>
      <p>Double: {{ doubled() }}</p>
    </div>
  `
})
export class CounterComponent {
  // シグナル更新はテンプレートに自動的に通知する
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  increment() {
    this.count.update(v => v + 1);
    // markForCheck()は不要 - シグナルが通知を処理する
  }

  decrement() {
    this.count.update(v => v - 1);
  }
}

シグナルはzonelessモードの自然な相棒です。シグナルの値が変更されると、Angularはそれに依存するテンプレートを正確に把握し、それらのビューのみに対してターゲットを絞った変更検知をスケジュールします。

OnPushは推奨されるが必須ではない

ChangeDetectionStrategy.OnPushの採用はzoneless互換性への推奨ステップですが、厳密には必須ではありません。デフォルトの変更検知戦略はzonelessモードでも機能します。ただし、OnPushはコンポーネントが入力が変更されたときまたはmarkForCheck()が呼び出されたときのみ再レンダリングされることを保証し、zonelessのメンタルモデルと自然に一致します。

既存アプリケーションの移行:一般的な落とし穴

Zone.jsからzonelessへの移行は、既存のアプリケーションにとってめったに1行の変更で済みません。Zone.jsの暗黙的な動作に依存していたいくつかのパターンには、明示的な処理が必要です。

setTimeoutとsetIntervalは更新をトリガーしない

Zone.jsモードでは、setTimeoutコールバックが自動的に変更検知をトリガーします。zonelessモードでは、トリガーしません。

user-status.component.ts - 以前:Zone.jsに依存typescript
@Component({
  selector: 'app-user-status',
  template: `<span>{{ statusMessage }}</span>`
})
export class UserStatusComponent {
  statusMessage = 'Loading...';

  ngOnInit() {
    setTimeout(() => {
      // Zone.jsはここでCDをトリガーする - zonelessはトリガーしない
      this.statusMessage = 'Ready';
    }, 2000);
  }
}
user-status.component.ts - 以後:シグナルベースのアプローチtypescript
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-user-status',
  template: `<span>{{ statusMessage() }}</span>`
})
export class UserStatusComponent {
  statusMessage = signal('Loading...');

  ngOnInit() {
    setTimeout(() => {
      // シグナル更新はAngularに自動的に通知する
      this.statusMessage.set('Ready');
    }, 2000);
  }
}

Reactive Formsには明示的な通知が必要

FormControl.setValue()またはpatchValue()を介したフォーム状態の変更は、zonelessモードでは自動的に変更検知をトリガーしません。2つのアプローチがこれを解決します:フォームのObservableをmarkForCheck()に接続するか、シグナルを介してフォームデータを公開します。

search.component.ts - zonelessでのReactive Formstypescript
import { Component, inject, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs';

@Component({
  selector: 'app-search',
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="searchControl" placeholder="Search..." />
    <p>Results for: {{ searchTerm() }}</p>
  `
})
export class SearchComponent {
  searchControl = new FormControl('');

  // Observableをシグナルに変換して自動テンプレート更新
  searchTerm = toSignal(
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged()
    ),
    { initialValue: '' }
  );
}

Angularの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Zone.jsなしのサーバーサイドレンダリング

zoneless AngularでのSSRには特別な注意が必要です。Zone.jsは以前、アプリケーションがシリアライズに適した「安定」状態に達したことをAngularが判断するのに役立ちました。それがなければ、PendingTasksサービスがその役割を果たします。

data-loader.component.ts - SSR互換の非同期ロードtypescript
import { Component, inject, signal } from '@angular/core';
import { PendingTasks } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'app-data-loader',
  template: `
    @if (data()) {
      <div>{{ data()!.title }}</div>
    } @else {
      <div>Loading...</div>
    }
  `
})
export class DataLoaderComponent {
  private http = inject(HttpClient);
  private pendingTasks = inject(PendingTasks);
  data = signal<{ title: string } | null>(null);

  ngOnInit() {
    // PendingTasks.run()は完了するまでSSRシリアライゼーションを防ぐ
    this.pendingTasks.run(async () => {
      const result = await firstValueFrom(
        this.http.get<{ title: string }>('/api/data')
      );
      this.data.set(result);
    });
  }
}

PendingTasksがなければ、Angularは非同期データがロードされる前にページをシリアライズし、空のコンテンツをクライアントに送信します。

パフォーマンスベンチマーク:Zone.js vs Zoneless

Zone.jsを削除することによるパフォーマンス向上は3つのカテゴリに分類されます。

| メトリック | Zone.js | Zoneless | 改善 | |--------|---------|----------|-------------| | 初期バンドルサイズ | +33KB生 / +10KB gzip | 0KBオーバーヘッド | 100%削減 | | 変更検知サイクル(典型的なアプリ) | インタラクションあたり150-300 | インタラクションあたり5-15 | 80-95%削減 | | スタックトレースの深さ | Zoneフレームが8-12追加 | クリーンなネイティブトレース | 即座の明確性 | | Time to Interactive(TTI) | ベースライン | 15-25%高速 | Zoneブートストラップを排除 |

最も劇的な改善は、大量の非同期アクティビティを持つアプリケーションで現れます:HTTPポーリング、WebSocket接続、タイマー、複雑なイベントハンドラー。これらはすべて、以前は不要な変更検知サイクルをトリガーしていましたが、zonelessモードでは完全に排除されます。

ライブラリの互換性

一部のサードパーティライブラリは内部的にZone.jsに依存しています。モーダルダイアログ、特定のWeb Componentラッパー、一部のアニメーションライブラリは、zonelessモードで予期しない動作をする可能性があります。本番環境にzonelessをデプロイする前に、常にライブラリの統合を徹底的にテストしてください。

移行後も存続するNgZone API

よくある誤解:Zone.jsを削除することは、すべてのNgZone参照を削除することを意味します。これは正しくありません。NgZone.run()NgZone.runOutsideAngular()はzonelessアプリケーションと互換性があり、共有ライブラリに保持する必要があります。これらを削除すると、Zone.jsを使用してこれらのライブラリを消費するアプリケーションでパフォーマンスの低下を引き起こす可能性があります。

ただし、3つのNgZone Observableは削除する必要があります。

  • NgZone.onMicrotaskEmpty - zonelessモードでは発行されない
  • NgZone.onUnstable - zonelessモードでは発行されない
  • NgZone.onStable - zonelessモードでは発行されない

これらのObservableを使用したタイミング依存のロジックを@angular/coreafterNextRender()またはafterEveryRender()に置き換えます。

Angular 19実験版からAngular 21デフォルトまで

zoneless APIの進化は明確な安定化パスに従います。

  • Angular 18.1: provideExperimentalZonelessChangeDetection()が実験的に導入
  • Angular 19: 実験的APIが洗練され、より広範なエコシステムテスト
  • Angular 20: provideZonelessChangeDetection()に名前変更、安定版に昇格
  • Angular 20.2: APIが完全に安定し、動作の変更は予想されない
  • Angular 21: zonelessがng newプロジェクトのデフォルトになり、プロバイダー呼び出しは不要

Angular 19を使用しているチームの場合、アップグレードパスは簡単です:Angular 20に更新し、provideExperimentalZonelessChangeDetectionprovideZonelessChangeDetectionに置き換え、zone.jsポリフィルを削除します。Angular 19は2026年5月19日にサポート終了となるため、この移行は時間的に重要です。

Angular面接の準備をしていますか?SharpSkillのAngular面接質問モジュールは、面接官がますます尋ねるzonelessシナリオを含む変更検知パターンを詳しくカバーしています。より広範な概要については、Angular面接質問トップ25ガイドをご覧ください。Angularシグナルモジュールも、zonelessを可能にする反応性モデルをカバーしています。

結論

  • Zone.jsを削除すると、33KBのバンドル重量が排除され、130以上のブラウザAPIをインターセプトしていたモンキーパッチング層が削除されます
  • Angularシグナルは、zonelessを実用的にする明示的な反応性モデルを提供し、状態が変更されたときにテンプレートに自動的に通知します
  • 移行には、setTimeout/setIntervalパターン、Reactive Forms、タイミング依存のロジックをシグナルベースまたはmarkForCheck()アプローチに変換する必要があります
  • SSRアプリケーションは、Zone.jsの安定性検出を置き換えるためにPendingTasksを採用する必要があります
  • NgZone.run()NgZone.runOutsideAngular()は、下位互換性のために共有ライブラリに保持する必要があります
  • Angular 19の実験的API(provideExperimentalZonelessChangeDetection)は、Angular 20の安定版provideZonelessChangeDetection()に直接マッピングされるため、アップグレードは名前変更操作になります
  • サードパーティライブラリの互換性は主要なリスク要因であり、本番環境へのデプロイ前に徹底的なテストが必要です

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#angular
#zoneless
#change-detection
#performance
#signals
#zone-js

共有

関連記事