Ideas for better tree-shaking of the Forms package

While doing investigation in a context of #40997, I came across a few areas in Forms module that can be refactored to provide better tree-shaking.

IMPORTANT: I’m creating this ticket primarily to capture some ideas/proposals. The numbers that are used in the ticket are for a particular use-case only, the real result might be different based on a particular scenario.

The use-case that I’ve looked at was an <input> with an ngModel on it: <input type="text" [(ngModel)="title">.

Currently (with Angular 11.2.2) the Forms package size for such use-case is 41.81kb. I’ve built an app using command from #40997 (comment), so it’s not a fully optimized version (with all optimizations it’s 30.11kb). While doing the investigation I prototyped some of the changes to get a sense of potential savings we can get from improved tree-shaking.

  1. [PR #41146] Built-in Control Value Accessors are not tree-shakable due to this code:
export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean {
return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a);

This can be refactored to be tree-shakable by adding a special field onto these classes (for ex ɵbuiltinCVA) and checking if this field exists in the isBuiltInAccessor function instead of referring to these classes directly.

Performing this refactoring allows to drop 10.12kb (~25%% reduction), so the Forms package reduced to 31.69kb for that use-case.

  1. [PR #41189] Extract some logic from the Validators class and use them directly in the Forms code.

Currently the Validators class contains some helper functions (such as Validators.compose, Validators.composeAsync and Validators.nullValidator) that are used across the code of the Forms package. We should consider extracting these functions, using them directly in the code (to avoid references to Validators class) and use these helpers in the Validators class too (to avoid any breaking changes).

This optimization allows to save 3.36kb (~8%% reduction), so the Forms package reduced to 28.33kb for that use-case.

  1. Avoid direct references to FormArray and FormGroup in the _find function.

The mentioned _find function is used in the AbstractControl, so once the FormControl is present in the code (which is used under the hood in the NgModel directive), the FormArray and FormGroup become non-tree-shakable as well (even if they are not used anywhere else). Possible refactoring it to do something similar to what I mentioned in the 1st section (regarding Built-in CVAs).

That can give another 7kb of savings (or 16.7%% reduction), so the Forms package size is 21.3kb.

  1. [PR #41126] I also noticed that the RadioControlRegistry class is non-tree-shakable (as it’s directly referenced in the FormsModule and ReactiveFormsModule). We can consider making it a tree-shakable provider (by using providedIn: FormsModule), but that might potentially be a breaking change.

Making RadioControlRegistry tree-shakable saves a bit less than 1kb, so the final Forms package size in my experiment was 20.5kb, which is half the size of the original package (41.81kb).

Note: in real scenarios savings would be much less than in my example, since most of the Forms code would actually be used (such as validators, FormGroup and FormArray, different types of inputs -> more CVAs), but improved tree-shaking should still provide benefits.

1 possible answer(s) on “Ideas for better tree-shaking of the Forms package

    1. [PR #41126] In addition to making the RadioControlRegistry provider tree-shakable (as mentioned above), we should also consider making FormBuilder tree-shakable as well, i.e.:
    @Injectable({providedIn: ReactiveFormsModule})
    export class FormBuilder { ... }

    Instead of referencing it directly here:

    providers: [FormBuilder, RadioControlRegistry],