Reactive Form 驗證效能優化
問題描述
在 Angular 的 Reactive Forms 中,當在模板中直接調用方法來檢查表單控制項的有效性時,會導致效能問題。這是因為:
每次變更檢測週期,這些方法都會被多次調用
對於表單中的每個欄位,這些方法都會被重複執行
這種方式會導致不必要的計算和頻繁的 DOM 更新

原始問題 code
Template 部分:
<input
#title
type="text"
name="title"
id="title"
[class.error]="isFieldInvalid('title')"
formControlName="title"
/>
<div class="input-hint">
<mat-hint>
<span [class.error]="isFieldInvalid('title')">{{
title.value.length.toLocaleString()
}}</span>
/{{ titleMaxLength.toLocaleString() }}</mat-hint
>
@if(isFieldInvalid('title')) {
<p class="error-message">{{ getFieldError("title") }}</p>
}
</div>
Component 部分:
// 如果控制項存在,且已經互動過,且其值無效,則返回 true(顯示錯誤訊息)
isFieldInvalid(fieldName: string, index: number = 0): boolean {
if (fieldName === 'options') {
const control = this.optionArray.controls[index];
return !!control && control.touched && control.invalid;
} else {
const control = this.createEventForm.get(fieldName);
return !!control && control.touched && control.invalid;
}
}
// 表單定義
createEventForm = new FormGroup({
title: new FormControl('', {
validators: [
Validators.required,
Validators.maxLength(this.titleMaxLength),
],
}),
description: new FormControl('', {
validators: [Validators.maxLength(this.descriptionMaxLength)],
}),
endTime: new FormControl(null, {
validators: [Validators.required, mustEarlierThanCurrent],
}),
options: new FormArray([
new FormControl('', {
validators: [
Validators.required,
Validators.maxLength(this.optionMaxLength),
],
}),
new FormControl('', {
validators: [
Validators.required,
Validators.maxLength(this.optionMaxLength),
],
}),
]),
});
優化方案比較
方案 1:使用 OnPush 變更檢測策略
@Component({
// ...
changeDetection: ChangeDetectionStrategy.OnPush,
})
優點:
實現簡單,只需添加一行程式碼
減少整體元件的變更檢測頻率
缺點:
只是減少了調用頻率,沒有解決重複執行的根本問題
僅適用於相對簡單的表單
方案 2:使用嵌套條件檢查
@if(title.dirty) {
@if(isFieldInvalid('title')) {
<p class="error-message">{{ getFieldError("title") }}</p>
}
}
優點:
實現簡單,不需要更改元件邏輯
只有當用戶與欄位互動後,才會執行驗證方法
可以與 OnPush 變更檢測結合使用
缺點:
只是減少了調用次數,同一變更檢測週期內仍有重複計算
模板程式碼變得更複雜
不符合反應式編程思想
方案 3:使用 getter 存取器
// 元件中定義 getter
get isTitleInvalid(): boolean {
const control = this.createEventForm.get('title');
return !!control && control.touched && control.invalid;
}
// 其他欄位的 getter...
<!-- 模板中使用 -->
<input [class.error]="isTitleInvalid" ... />
@if(isTitleInvalid) {
<p class="error-message">{{ getFieldError("title") }}</p>
}
優點:
模板程式碼更簡潔易讀
將驗證邏輯集中在元件類中
比方法調用看起來更符合聲明式編程風格
缺點:
需要為每個欄位創建單獨的 getter
仍然會在每次變更檢測時重新計算
從效能角度看與普通方法沒有本質區別
方案 4:使用 Signal(最佳解決方案)
Angular 的 Signal API 提供了一種反應式的方式來處理數據,具有記憶化(memoization)功能,能有效減少重複計算。
步驟 1:定義表單驗證狀態介面
interface FieldValidationState {
isInvalid: boolean;
errorMsg: string | null;
}
interface FormValidationState {
title: FieldValidationState;
description: FieldValidationState;
endTime: FieldValidationState;
options: FieldValidationState[];
}
步驟 2:在元件中創建 Signal
// 在元件類中添加
private formValidation = signal<FormValidationState>({
title: { isInvalid: false, errorMsg: null },
description: { isInvalid: false, errorMsg: null },
endTime: { isInvalid: false, errorMsg: null },
options: [
{ isInvalid: false, errorMsg: null },
{ isInvalid: false, errorMsg: null }
]
});
// 提供給模板的唯讀 Signal
readonly formState = this.formValidation.asReadonly();
步驟 3:監聽表單變化並更新 Signal
ngOnInit() {
// 監聽表單狀態變化
this.subscribeToFormChanges();
}
private subscribeToFormChanges() {
// 監聽常規欄位狀態變化
this.createEventForm.get('title')?.statusChanges.subscribe(() => {
this.updateFieldValidation('title');
});
this.createEventForm.get('description')?.statusChanges.subscribe(() => {
this.updateFieldValidation('description');
});
this.createEventForm.get('endTime')?.statusChanges.subscribe(() => {
this.updateFieldValidation('endTime');
});
// 監聽選項陣列狀態變化
this.optionArray.statusChanges.subscribe(() => {
this.updateOptionsValidation();
});
// 監聽選項陣列長度變化
this.optionArray.valueChanges.subscribe(() => {
if (this.optionArray.length !== this.formValidation().options.length) {
this.updateOptionsValidation();
}
});
}
步驟 4:實現更新驗證狀態的方法
// 更新一般欄位的驗證狀態
private updateFieldValidation(fieldName: 'title' | 'description' | 'endTime') {
const control = this.createEventForm.get(fieldName);
if (!control) return;
const isInvalid = control.touched && control.invalid;
const errorMsg = isInvalid ? this.generateErrorMessage(fieldName) : null;
// 更新 Signal
this.formValidation.update(state => ({
...state,
[fieldName]: { isInvalid, errorMsg }
}));
}
// 更新選項陣列的驗證狀態
private updateOptionsValidation() {
const optionsState = this.optionArray.controls.map((control, index) => {
const isInvalid = control.touched && control.invalid;
const errorMsg = isInvalid ? this.generateErrorMessage('options', index) : null;
return { isInvalid, errorMsg };
});
// 更新 Signal
this.formValidation.update(state => ({
...state,
options: optionsState
}));
}
步驟 5:在模板中使用 Signal
<input
#title
type="text"
name="title"
id="title"
[class.error]="formState().title.isInvalid"
formControlName="title"
/>
<div class="input-hint">
<mat-hint>
<span [class.error]="formState().title.isInvalid">{{
title.value.length.toLocaleString()
}}</span>
/{{ titleMaxLength.toLocaleString() }}</mat-hint
>
@if(formState().title.isInvalid) {
<p class="error-message">{{ formState().title.errorMsg }}</p>
}
</div>
優點:
真正解決了重複計算的問題
符合反應式編程的思想
表單狀態變更時才會更新 Signal,避免了不必要的計算
與 OnPush 變更檢測策略完美配合
缺點:
初始設置較為複雜
需要手動管理訂閱(記得在 ngOnDestroy 中取消訂閱)
測試
原始方法
private callCount = 0;
isFieldInvalid(fieldName: string, index: number = 0): boolean {
this.callCount++;
console.log(`isFieldInvalid called ${this.callCount} times,欄位: ${fieldName}`);
// 原始邏輯...
}

Signal 方法
private updateCount = 0;
private updateFieldValidation(fieldName: 'title' | 'description' | 'endTime') {
this.updateCount++;
console.log(`updateFieldValidation is called ${this.callCount} times);
// Signal 更新邏輯...
}

送出同樣內容的表單,以一樣的操作,可以得到以下數據
函式調用總次數
478
11
97.7%
最佳實踐建議
對於簡單的表單:
使用 OnPush 變更檢測策略 + getter 屬性存取器可能已經足夠
對於中等複雜度的表單:
使用 OnPush 變更檢測策略 + 嵌套條件檢查
對於複雜的表單或高性能需求:
使用 Signal API 是最佳選擇,儘管設置較為複雜,但能提供最佳性能
其他優化技巧:
避免在模板中直接調用複雜的方法
將計算密集的操作移到元件類中
對於表單狀態變化,考慮使用 RxJS 的
distinctUntilChanged
運算符來減少不必要的更新善用 Angular 的
NG_VALUE_ACCESSOR
和自定義表單控制項來封裝複雜的表單邏輯考慮使用
trackBy
函數來優化*ngFor
迴圈中的表單元素
注意事項
使用 Signal API 需要 Angular 16+ 版本
使用 Signal 時,需要妥善處理訂閱以避免記憶體洩漏
過度優化可能導致程式碼複雜性增加,應根據實際性能需求選擇適當的優化方案
在優化之前,建議先使用 Angular DevTools 或瀏覽器的性能分析工具確認瓶頸
Last updated