前言

在 Angular 開發中,我們經常需要動態調整元素的屬性。但你知道嗎?不當的綁定方式可能會影響應用程式的效能。這篇文章將探討如何正確使用屬性綁定,並介紹幾種優化效能的策略。

問題背景

在 Angular 的 HTML 模板中,我們常常會使用屬性綁定來動態調整元素的屬性,例如:

<div [style.width]="getWidth()"></div>

這種方式讓我們可以根據程式的狀態或用戶的互動來動態調整元素的寬度。但是當畫面有變動時,Angular 的變更檢測機制會重新評估模板中的所有綁定表達式,其中也包括函數調用。

這意味著如果在模板中使用了 [style.width]="getWidth()",每次變更檢測運行時,getWidth() 函數都會被呼叫。

雖然這種行為在大多數情況下不會導致顯著的性能問題,但如果 getWidth() 函數的計算成本很高,或者它被頻繁地調用,這可能會導致性能下降。

為了避免這種情況,可以採用以下幾種策略。

優化策略

1. 使用變數而非函數

可以將 getWidth() 的結果存儲在一個變數中,並在需要時更新這個變數。只有在變數的值真正改變時,Angular 才會重新評估綁定表達式。

<div [style.width]="width"></div>
export class MyComponent {
  width: string = "100px";

  updateWidth(newWidth: string): void {
    this.width = newWidth;
  }
}

優點

  • 減少函數調用次數
  • 變更檢測更有效率
  • 程式碼更清晰易讀

2. 使用 Observable 和 async 管道

如果 getWidth() 的結果來自於異步操作(例如:HTTP request),可以使用 Observable 和 async 管道來自動處理變更檢測。只有當 Observable 發出新的值時,Angular 才會更新綁定的屬性。

<div [style.width]="width$ | async"></div>
export class MyComponent {
  width$: Observable<string>;

  constructor(private http: HttpClient) {
    this.width$ = this.http.get<{width: string}>('/api/width')
      .pipe(map(response => response.width));
  }
}

優點

  • 自動處理訂閱和取消訂閱
  • 避免記憶體洩漏
  • 與 RxJS 生態系統整合良好

3. 使用 OnPush 變更檢測策略

可以將組件的變更檢測策略設置為 ChangeDetectionStrategy.OnPush。Angular 只會在輸入屬性改變事件觸發時才進行變更檢測,而不是在每次變更檢測運行時都重新評估所有綁定表達式。

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: "app-my-component",
  templateUrl: "./my-component.component.html",
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
  width: string = "100px";
}

優點

  • 大幅提升效能
  • 減少不必要的變更檢測
  • 更可預測的行為

注意事項

  • 需要確保正確觸發變更檢測
  • 可以搭配 ChangeDetectorRef.markForCheck() 手動觸發

4. 使用 trackBy 函數

如果我們在模板中使用 *ngFor 來迭代一個列表,並且每次變更檢測都會重新生成 DOM 元素,我們可以使用 trackBy 函數來提高性能。

trackBy 函數可以告訴 Angular 如何追蹤列表中的項目,從而避免不必要的 DOM 操作。

<div *ngFor="let item of items; trackBy: trackByFn">
  {{ item.name }}
</div>
export class MyComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];

  trackByFn(index: number, item: any): number {
    return item.id;  // 使用唯一 ID 來追蹤項目
  }
}

優點

  • 減少 DOM 操作
  • 提升列表渲染效能
  • 保留元素狀態(如 input 的輸入值)

5. 使用 Memoization

對於計算成本高的函數,可以使用 memoization 技術來快取結果:

export class MyComponent {
  private widthCache = new Map<string, string>();

  getWidth(key: string): string {
    if (!this.widthCache.has(key)) {
      // 進行複雜的計算
      const width = this.calculateWidth(key);
      this.widthCache.set(key, width);
    }
    return this.widthCache.get(key)!;
  }

  private calculateWidth(key: string): string {
    // 複雜的計算邏輯
    return '100px';
  }
}

6. 使用 Signal(Angular 16+)

Angular 16 引入的 Signal 提供了更精確的變更追蹤:

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-my-component',
  template: '<div [style.width]="width()"></div>'
})
export class MyComponent {
  baseWidth = signal(100);
  width = computed(() => `${this.baseWidth()}px`);

  updateWidth(newWidth: number): void {
    this.baseWidth.set(newWidth);
  }
}

效能比較

方法變更檢測頻率效能使用難度
函數調用每次檢測簡單
變數綁定值改變時⭐⭐⭐⭐簡單
Observable + async值發出時⭐⭐⭐⭐中等
OnPush 策略輸入改變時⭐⭐⭐⭐⭐中等
trackBy列表變化時⭐⭐⭐⭐⭐簡單
Signal值改變時⭐⭐⭐⭐⭐簡單

總結

這些策略可以幫助我們優化 Angular 應用程式的性能,特別是在處理大量數據或進行頻繁的 DOM 操作時。

建議做法

  1. 優先使用變數而非函數:簡單且有效
  2. 善用 OnPush 策略:大幅提升效能
  3. 使用 trackBy:列表渲染的必備技巧
  4. 考慮使用 Signal:新專案的最佳選擇
  5. 搭配 async 管道:處理異步資料的標準做法

我們可以根據具體情況選擇適合的策略,並根據具體需求進行調整,可以幫助我們避免不必要的性能問題,打造更流暢的使用者體驗。

參考資料