Angular 19 Zoneless: Hiệu năng và Change Detection không cần Zone.js

Angular zoneless change detection loại bỏ Zone.js để mang lại bundle nhỏ hơn, rendering nhanh hơn và mô hình reactivity tường minh thông qua signals. Hướng dẫn chuyên sâu về quá trình chuyển đổi từ Zone.js sang zoneless Angular, từ API experimental trong Angular 19 đến API stable trong Angular 20+.

Sơ đồ kiến trúc Angular zoneless change detection với signals và tối ưu hiệu năng

Angular 19 đánh dấu một bước ngoặt quan trọng trong lịch sử phát triển của framework khi giới thiệu chế độ Zoneless — cho phép ứng dụng hoạt động hoàn toàn không cần Zone.js. Kể từ khi ra đời, Zone.js luôn đóng vai trò trung tâm trong cơ chế Change Detection của Angular, tuy nhiên nó cũng mang theo những hạn chế đáng kể về hiệu năng và kích thước bundle. Với Angular 19, đội ngũ phát triển đã cung cấp một giải pháp thay thế dựa trên Signals, mở ra kỷ nguyên mới cho việc xây dựng ứng dụng Angular nhanh hơn, nhẹ hơn và dễ debug hơn.

Zoneless đã ổn định từ Angular 20

Chế độ Zoneless được giới thiệu ở trạng thái experimental trong Angular 19 với API provideExperimentalZonelessChangeDetection(). Từ Angular 20 trở đi, API này đã chính thức ổn định với tên gọi provideZonelessChangeDetection(). Các dự án mới hoàn toàn có thể áp dụng ngay từ đầu, trong khi các dự án hiện tại nên lên kế hoạch chuyển đổi dần dần.

Zone.js là gì và tại sao cần loại bỏ nó?

Zone.js là một thư viện đã gắn liền với Angular từ phiên bản 2. Nó hoạt động bằng cách monkey-patch hơn 130 API trình duyệt — bao gồm setTimeout, Promise, addEventListener, XMLHttpRequest và nhiều API khác — để tạo ra một "execution context" cho phép Angular biết khi nào cần chạy Change Detection.

Mỗi khi một callback bất đồng bộ được thực thi trong zone của Angular, framework sẽ tự động kích hoạt một chu kỳ Change Detection trên toàn bộ cây component. Cơ chế này tuy tiện lợi nhưng gây ra nhiều vấn đề nghiêm trọng:

  • Kích thước bundle tăng thêm ~33KB (raw) hoặc ~10KB (gzip) chỉ riêng cho Zone.js
  • Change Detection chạy quá nhiều lần không cần thiết, đặc biệt trong các ứng dụng phức tạp với nhiều thao tác bất đồng bộ
  • Stack trace bị "ô nhiễm" bởi 8-12 frame thừa từ Zone.js, khiến việc debug trở nên khó khăn
  • Xung đột với các thư viện bên thứ ba không tương thích với cơ chế patching của Zone.js

Dưới đây là cấu hình truyền thống sử dụng Zone.js:

app.config.ts - Traditional Zone.js setup (Angular 18-19)typescript
import { ApplicationConfig } from '@angular/core';
import { provideZoneChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    // Zone.js patches ~130+ browser APIs
    // Every async callback triggers change detection
  ]
};

Với mỗi sự kiện click, mỗi HTTP request hoàn thành, mỗi setTimeout kết thúc — Zone.js đều thông báo cho Angular và kích hoạt kiểm tra toàn bộ cây component. Trong một ứng dụng enterprise với hàng trăm component, điều này dẫn đến hàng trăm chu kỳ Change Detection không cần thiết cho mỗi tương tác của người dùng.

Kích hoạt chế độ Zoneless trong Angular 19

Việc chuyển sang chế độ Zoneless khá đơn giản. Trong Angular 19, API được đánh dấu là experimental:

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    // No more Zone.js patching
  ]
};

Từ Angular 20 trở đi, API đã được ổn định hóa:

app.config.ts - Angular 20+ (stable)typescript
import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';

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

Sau khi cập nhật cấu hình, cần loại bỏ Zone.js khỏi dự án:

bash
# Remove zone.js polyfill from angular.json build and test targets
# Then uninstall the package
npm uninstall zone.js

Cần lưu ý rằng việc loại bỏ Zone.js cũng đồng nghĩa với việc phải xóa các tham chiếu đến zone.js/testing trong file cấu hình test, cũng như loại bỏ entry zone.js trong mảng polyfills của angular.json.

So sánh hiệu năng: Zone.js và Zoneless

Sự khác biệt về hiệu năng giữa hai chế độ là rất đáng kể, đặc biệt trong các ứng dụng quy mô lớn:

| Metric | Zone.js | Zoneless | Improvement | |--------|---------|----------|-------------| | Initial bundle size | +33KB raw / +10KB gzip | 0KB overhead | 100% reduction | | Change detection cycles (typical app) | 150-300 per interaction | 5-15 per interaction | 80-95% fewer | | Stack trace depth | 8-12 extra Zone frames | Clean native traces | Immediate clarity | | Time to Interactive (TTI) | Baseline | 15-25% faster | Zone bootstrap eliminated |

Những con số trên cho thấy chế độ Zoneless không chỉ giảm kích thước bundle mà còn cải thiện đáng kể trải nghiệm runtime. Việc giảm 80-95% số chu kỳ Change Detection đồng nghĩa với việc CPU của người dùng được giải phóng đáng kể, đặc biệt quan trọng trên các thiết bị di động có cấu hình thấp.

Signals — nền tảng của Zoneless Change Detection

Signals là cơ chế reactivity được Angular giới thiệu từ phiên bản 16 và trở thành trụ cột chính của chế độ Zoneless. Thay vì dựa vào Zone.js để "đoán" khi nào dữ liệu thay đổi, Signals cho phép Angular biết chính xác giá trị nào đã thay đổi và component nào cần cập nhật.

counter.component.ts - Signal-driven change detectiontypescript
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 {
  // Signal updates automatically notify the template
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  increment() {
    this.count.update(v => v + 1);
    // No markForCheck() needed - signal handles notification
  }

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

Trong ví dụ trên, khi count thay đổi, Angular chỉ cập nhật đúng phần template sử dụng count()doubled(). Không có chu kỳ Change Detection toàn cục nào được kích hoạt — chỉ những component thực sự phụ thuộc vào signal đó mới được đánh dấu cần kiểm tra.

OnPush không còn cần thiết với Zoneless

Trong mô hình Zone.js, chiến lược ChangeDetectionStrategy.OnPush được sử dụng rộng rãi để tối ưu hiệu năng bằng cách hạn chế Change Detection chỉ chạy khi Input thay đổi hoặc khi gọi markForCheck(). Với chế độ Zoneless và Signals, việc này trở nên không cần thiết — Angular tự động biết chính xác component nào cần cập nhật thông qua dependency tracking của Signals. Tuy nhiên, việc giữ OnPush trong code cũ không gây ra vấn đề gì và vẫn hoạt động bình thường.

Chuyển đổi code hiện tại sang Signals

Một trong những thách thức lớn nhất khi áp dụng Zoneless là chuyển đổi code hiện tại từ mô hình dựa trên Zone.js sang Signals. Dưới đây là ví dụ minh họa cho quá trình chuyển đổi này.

Trước — code dựa vào Zone.js:

user-status.component.ts - Before: relies on Zone.jstypescript
@Component({
  selector: 'app-user-status',
  template: `<span>{{ statusMessage }}</span>`
})
export class UserStatusComponent {
  statusMessage = 'Loading...';

  ngOnInit() {
    setTimeout(() => {
      // Zone.js would trigger CD here - zoneless does NOT
      this.statusMessage = 'Ready';
    }, 2000);
  }
}

Trong đoạn code trên, khi Zone.js bị loại bỏ, việc gán trực tiếp this.statusMessage = 'Ready' sẽ không kích hoạt cập nhật giao diện. Angular không có cách nào biết rằng giá trị đã thay đổi.

Sau — sử dụng Signals:

user-status.component.ts - After: signal-based approachtypescript
import { Component, signal } from '@angular/core';

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

  ngOnInit() {
    setTimeout(() => {
      // Signal update notifies Angular automatically
      this.statusMessage.set('Ready');
    }, 2000);
  }
}

Sự thay đổi rất đơn giản: thay thế thuộc tính thông thường bằng signal(), sử dụng set() hoặc update() để thay đổi giá trị, và thêm () khi đọc giá trị trong template. Angular sẽ tự động phát hiện sự thay đổi thông qua cơ chế subscription nội bộ của Signals.

Tích hợp Reactive Forms với Zoneless

Reactive Forms vốn dựa trên RxJS Observables. Để hoạt động tốt trong chế độ Zoneless, cần chuyển đổi Observable thành Signal bằng hàm toSignal():

search.component.ts - Reactive forms with zonelesstypescript
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('');

  // Convert observable to signal for automatic template updates
  searchTerm = toSignal(
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged()
    ),
    { initialValue: '' }
  );
}

Hàm toSignal() từ @angular/core/rxjs-interop đóng vai trò cầu nối giữa thế giới RxJS và Signals. Nó tự động subscribe Observable và cập nhật signal mỗi khi có giá trị mới, đồng thời tự động unsubscribe khi component bị destroy. Đây là pattern quan trọng nhất khi làm việc với Reactive Forms trong chế độ Zoneless.

Sẵn sàng chinh phục phỏng vấn Angular?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Xử lý SSR và tác vụ bất đồng bộ với PendingTasks

Trong chế độ Zoneless, Angular không còn tự động theo dõi các tác vụ bất đồng bộ thông qua Zone.js. Điều này đặc biệt quan trọng với Server-Side Rendering (SSR), nơi Angular cần biết khi nào tất cả dữ liệu đã sẵn sàng trước khi serialize HTML.

Angular cung cấp service PendingTasks để giải quyết vấn đề này:

data-loader.component.ts - SSR-compatible async loadingtypescript
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() prevents SSR serialization until complete
    this.pendingTasks.run(async () => {
      const result = await firstValueFrom(
        this.http.get<{ title: string }>('/api/data')
      );
      this.data.set(result);
    });
  }
}

Phương thức PendingTasks.run() đăng ký tác vụ bất đồng bộ với Angular, đảm bảo quá trình SSR sẽ chờ cho đến khi tác vụ hoàn thành trước khi gửi HTML về client. Đây là sự thay thế trực tiếp cho vai trò mà Zone.js từng đảm nhiệm trong việc theo dõi tác vụ async phục vụ SSR.

Kiểm tra tương thích thư viện bên thứ ba

Một số thư viện Angular phổ biến vẫn phụ thuộc vào Zone.js để hoạt động chính xác. Trước khi chuyển sang Zoneless, cần kiểm tra kỹ các thư viện đang sử dụng trong dự án. Các thư viện sử dụng setTimeout, setInterval hoặc gán trực tiếp thuộc tính mà không thông qua Signals hay ChangeDetectorRef.markForCheck() sẽ không cập nhật giao diện trong chế độ Zoneless. Danh sách thư viện tương thích đang được Angular team cập nhật liên tục.

Chiến lược chuyển đổi cho dự án hiện tại

Việc chuyển đổi một dự án lớn sang Zoneless không nên thực hiện một lần mà cần có lộ trình rõ ràng:

Giai đoạn 1 — Chuẩn bị: Chuyển toàn bộ component sang chiến lược OnPush. Điều này giúp phát hiện sớm các component đang phụ thuộc ngầm vào Change Detection mặc định. Thay thế các thuộc tính thông thường bằng Signals trong template.

Giai đoạn 2 — Hybrid mode: Angular hỗ trợ chạy Zoneless trong khi vẫn giữ Zone.js. Kích hoạt provideExperimentalZonelessChangeDetection() nhưng chưa xóa Zone.js khỏi polyfills. Điều này cho phép kiểm tra từng component một.

Giai đoạn 3 — Loại bỏ hoàn toàn: Sau khi đảm bảo tất cả component hoạt động đúng, xóa Zone.js khỏi angular.json và chạy npm uninstall zone.js.

Quá trình này có thể mất vài tuần đến vài tháng tùy quy mô dự án, nhưng lợi ích về hiệu năng và khả năng bảo trì là hoàn toàn xứng đáng.

Kết luận

Chế độ Zoneless trong Angular 19 đại diện cho một trong những cải tiến kiến trúc quan trọng nhất kể từ khi Angular 2 ra đời. Bằng việc loại bỏ Zone.js và chuyển sang mô hình reactivity dựa trên Signals, Angular đã giải quyết được nhiều vấn đề tồn đọng từ lâu:

  • Giảm kích thước bundle — loại bỏ hoàn toàn ~33KB overhead từ Zone.js, cải thiện thời gian tải trang đặc biệt trên thiết bị di động
  • Tối ưu Change Detection — giảm 80-95% số chu kỳ Change Detection không cần thiết, chỉ cập nhật đúng component cần thiết thông qua Signals
  • Cải thiện trải nghiệm debug — stack trace sạch sẽ, không còn các frame thừa từ Zone.js gây nhiễu khi tìm lỗi
  • Tương thích tốt hơn với SSR — thông qua PendingTasks, việc quản lý tác vụ bất đồng bộ trở nên tường minh và đáng tin cậy hơn
  • Hội nhập với hệ sinh thái web hiện đại — Signals là mô hình reactivity đang được nhiều framework áp dụng, giúp Angular gần hơn với các tiêu chuẩn web

Để nắm vững kiến thức về Change Detection và chuẩn bị cho các cuộc phỏng vấn Angular, tham khảo thêm câu hỏi phỏng vấn Angular hoặc bộ 25 câu hỏi phỏng vấn Angular hàng đầu. Ngoài ra, module Angular Signals cung cấp các bài tập thực hành chuyên sâu về Signals — nền tảng cốt lõi của chế độ Zoneless.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

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

Chia sẻ

Bài viết liên quan