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:

  1. Validation on Blur: This strategy triggers validation only when the user leaves a field (i.e., when the control loses focus).

  2. 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",
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. 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>
Enter fullscreen mode Exit fullscreen mode
trackByIndex(index: number): number {
   return index;
}
Enter fullscreen mode Exit fullscreen mode
  1. 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),
  },
];
Enter fullscreen mode Exit fullscreen mode

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