Why Performance Tuning Matters for Angular Forms
Forms lie at the heart of most web applications, and as they grow in size and complexity, performance issues begin to surface. Slow response times, unnecessary validation runs, and frustrated users are common symptoms when forms aren't optimized.
Several factors make optimization essential:
- Better user experience: Cutting down on redundant re-renders and inefficient validation checks translates to a snappier, more fluid interaction for users.
- Lower validation overhead: Running validation logic only when absolutely required can save a considerable amount of processing time.
- Scalability: Large forms with nested groups, dynamic controls, and custom validators can grind to a halt without careful performance considerations.
Lazy Validation: Deferring Checks Until They're Needed
Understanding Lazy Validation
Angular Reactive Forms, by default, run validation on every single value change. For simple forms, this is fine. But when you're dealing with intricate structures—nested form groups, custom validators, or many controls—this default behavior can become a serious drag on performance.
Lazy validation offers a solution: postpone validation until the moment it's actually required. This reduces the frequency of validation checks and keeps your forms running smoothly.
Implementing Lazy Validation
You can steer form controls away from the default "validate on every change" behavior by configuring them to validate under specific conditions:
Validation on Blur: This strategy triggers validation only when the user leaves a field (i.e., when the control loses focus).
Validation on Demand: This gives you full control, allowing validation to be triggered manually for the whole form or specific controls—for example, during form submission.
Let's walk through examples of both approaches.
Example: Validation on Blur
import { Component } from "@angular/core";
import { FormControl, Validators } from "@angular/forms";
@Component({
selector: "app-lazy-validation-form",
template: `
<form>
<label for="username">Username:</label>
<input id="username" [formControl]="usernameControl" />
<div *ngIf="usernameControl.invalid && usernameControl.touched">
Username is required and must be at least 3 characters long.
</div>
</form>
`,
})
export class LazyValidationFormComponent {
usernameControl = new FormControl("", {
validators: [Validators.required, Validators.minLength(3)],
updateOn: "blur",
});
}
Here, the updateOn: 'blur' configuration is used, meaning the username field will only be validated after the user moves focus away from it. This stops the control from validating with every keystroke.
Example: Triggering Validation on Submit
import { Component } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
@Component({
selector: "app-manual-validation-form",
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label for="email">Email:</label>
<input id="email" formControlName="email" />
<div *ngIf="form.controls.email.invalid && form.controls.email.touched">
Please enter a valid email.
</div>
<label for="password">Password:</label>
<input id="password" type="password" formControlName="password" />
<div
*ngIf="form.controls.password.invalid && form.controls.password.touched"
>
Password is required and must be at least 6 characters.
</div>
<button type="submit">Submit</button>
</form>
`,
})
export class ManualValidationFormComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
email: ["", [Validators.required, Validators.email]],
password: ["", [Validators.required, Validators.minLength(6)]],
});
}
onSubmit() {
if (this.form.invalid) {
// Manually mark all controls as touched to trigger validation
this.form.markAllAsTouched();
return;
}
// Form is valid, proceed with submission
console.log("Form submitted", this.form.value);
}
}
In this case, validation is invoked manually at the point of form submission. If the form is found to be invalid, all controls are marked as "touched" to reveal validation messages. This method ensures validation happens at a single, logical point, sidestepping any extra work while the user is in the process of filling out the form.
Async Validators: Integrating External Data Checks
Async validators are indispensable when form input needs to be checked against an external data source—like verifying a username or email isn't already in use via a backend API. Unlike their synchronous counterparts, async validators can return a promise or an observable, which makes handling asynchronous logic within validation straightforward.
The Advantages of Async Validators
- Asynchronous and Non-Blocking: They allow validation to happen without blocking the main UI thread, unlike heavy synchronous logic.
- Seamless API Integration: They enable direct validation against backend systems, such as ensuring a username is unique.
Building an Async Validator
Let's create an async validator that checks for username availability by calling a backend service.
Example: Async Validator for Username Availability
import { Injectable } from "@angular/core";
import { AbstractControl, AsyncValidatorFn } from "@angular/forms";
import { Observable, of } from "rxjs";
import { catchError, debounceTime, map, switchMap } from "rxjs/operators";
import { HttpClient } from "@angular/common/http";
@Injectable({ providedIn: "root" })
export class UsernameValidator {
constructor(private http: HttpClient) {}
validateUsername(): AsyncValidatorFn {
return (
control: AbstractControl
): Observable<{ [key: string]: any } | null> => {
return control.valueChanges.pipe(
debounceTime(300),
switchMap((username) => this.checkUsername(username)),
map((isTaken) => (isTaken ? { usernameTaken: true } : null)),
catchError(() => of(null))
);
};
}
private checkUsername(username: string): Observable<boolean> {
// Simulating an API call to check if the username is taken
return this.http.get<boolean>(`/api/check-username?username=${username}`);
}
}
This example leverages RxJS operators including debounceTime and switchMap to prevent a flood of API calls and to make sure the backend is only queried once the user has paused typing. The validator returns an error object (usernameTaken: true) when the username is already in use, otherwise it returns null.
Attaching the Async Validator to a Form Control
import { Component } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { UsernameValidator } from "./username-validator.service";
@Component({
selector: "app-async-validator-form",
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label for="username">Username:</label>
<input id="username" formControlName="username" />
<div
*ngIf="form.controls.username.invalid && form.controls.username.touched"
>
<div *ngIf="form.controls.username.errors?.usernameTaken">
Username is already taken.
</div>
<div *ngIf="form.controls.username.errors?.required">
Username is required.
</div>
</div>
<button type="submit">Submit</button>
</form>
`,
})
export class AsyncValidatorFormComponent {
form: FormGroup;
constructor(
private fb: FormBuilder,
private usernameValidator: UsernameValidator
) {
this.form = this.fb.group({
username: [
"",
[Validators.required],
[this.usernameValidator.validateUsername()],
],
});
}
onSubmit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log("Form submitted", this.form.value);
}
}
Here, the username control is set up with both a standard synchronous validator (Validators.required) and the custom async validator (this.usernameValidator.validateUsername()). Upon form submission, this ensures the form is blocked if the username is already taken.
Enhancing Form Rendering Performance
Optimization isn't just about validation. How Angular Reactive Forms are rendered also plays a huge role in app performance. Angular's default change detection checks all components on every input event, which can become a significant performance drain in larger apps.
Consider these strategies to improve form rendering:
- Adopt the OnPush Change Detection Strategy
Switching from the Default strategy to the OnPush strategy limits change detection to only run when a component's input properties change or an event is triggered within the component, resulting in fewer checks and better performance.
Code example:
@Component({
selector: "app-optimized-form",
templateUrl: "./optimized-form.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedFormComponent {
// Component logic
}
- Leverage trackBy with ngFor Loops
When you're rendering lists of form controls—like in dynamic forms or repeated fields—using trackBy with ngFor can stop Angular from re-rendering the entire list when just one item changes.
Example usage:
<div *ngFor="let control of formArray.controls; trackBy: trackByIndex">
<input [formControl]="control" />
</div>
trackByIndex(index: number): number {
return index;
}
- Implement Lazy Loading for Form Modules
If your app has complex forms across multiple routes, lazy-loading the modules that contain those forms can dramatically cut the initial load time. The forms are then only fetched when the user actually navigates to them.
Routing example:
const routes: Routes = [{
path: "user-form",
loadChildren: () =>
import("./user-form/user-form.module").then((m) => m.UserFormModule),
},
];
Wrapping Up
The performance of Angular Reactive Forms is a critical factor in building applications that are efficient, responsive, and scalable. By adopting lazy validation, integrating async validators, and applying rendering optimizations, you can achieve significant performance gains, particularly in complex scenarios with nested structures and external data validation.
These techniques not only make your forms faster but also more manageable and maintainable, which benefits both the developers building them and the users interacting with them.
For any application dealing with large-scale or dynamic forms, investing time in these optimization strategies will yield noticeable improvements in performance and overall user satisfaction.
🔗 Stay in Touch
Enjoyed reading this piece? Let’s stay connected:
🔗 Reach out on LinkedIn
💻 Explore my GitHub projects
☕ Support my work with a coffee
