Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
form() includes a safety check: if a rule reads another field with valueOf() and that field's value is a reactive forms control, it throws NG1907 and suggests compatForm(). The check uses instanceof AbstractControl. That reference alone keeps AbstractControl in the production bundle, and AbstractControl brings Validators, the control event classes, and parts of RxJS with it. This happens in every app that calls form(), including apps that never import reactive forms.
Why it matters. Apps that use only signal forms pay for reactive forms code they never run. A minimal app pays about 4.7 KB gzipped (10.7%) for code it never runs. An app that binds fields with [formField] pays about 1.9 KB (3.3%).
Example. A minimal app that uses only signal forms:
import {Component, provideZonelessChangeDetection, signal} from '@angular/core';
import {form, required} from '@angular/forms/signals';
import {bootstrapApplication} from '@angular/platform-browser';
@Component({
selector: 'app-root',
template: `{{ f.name().value() }} {{ f.name().valid() }}`,
})
class App {
readonly f = form(signal({name: ''}), (p) => {
required(p.name);
});
}
bootstrapApplication(App, {providers: [provideZonelessChangeDetection()]});Current behavior (production build, gzipped main.js):
44,490 B
includes AbstractControl, Validators, the control event classes, RxJS forkJoin and from
Expected behavior (my assumption):
39,742 B
without AbstractControl, Validators, the control event classes, or RxJS forkJoin and from
The expected size is what I measured with the check limited to development builds. This assumes that the NG1907 check is a development aid that doesn't need to run in production, which is how many Angular diagnostics work. If the team wants it in production, a check that doesn't reference the class gives the same size. I've listed this decision in the triage PR.
Please provide a link to a minimal reproduction of the bug
Measurement apps: forms_signals_model and forms_signals_field, in triage PR #71624. The PR changes no library code.
pnpm bazel build //packages/core/test/bundling/forms_signals_model:bundles //packages/core/test/bundling/forms_signals_field:bundlesPlease provide the exception or error you saw
There's no error. These are the production bundle sizes in bytes:
App Today (raw / gzip / brotli) Check in dev mode only (raw / gzip / brotli) Saved (gzip)
model only 141,563 / 44,490 / 39,869 124,217 / 39,742 / 35,722 4,748 B (10.7%)
[formField] 182,983 / 56,835 / 50,450 173,946 / 54,939 / 48,812 1,896 B (3.3%)
Please provide the environment you discovered this bug in (run ng version)
angular/angular main at 3e173fc844c440775d3f110da8505442e227dee5 (2026-10-24), built from source
Build: optimize_angular_app (production Angular CLI build), default settings
OS: macOS (darwin arm64)
Anything else?
Affected areas:
- Every app that calls
form(), whether or not any rule callsvalueOf(). - Apps that use
[formField]gain less from a fix: they still shipValidators,NgControl, and a few other reactive forms classes through theControlValueAccessorinterop. That's a separate, larger cleanup. - Apps that use
compatForm()already depend on reactive forms, so they gain nothing. - With the recommended fix, the
NG1907error throws only in development. In production, a rule that reads aFormControlthroughvalueOf()would get the control back instead of an error.
Fix options and tradeoffs. The PR has code for each option.
| Option | What changes | Tradeoffs |
|---|---|---|
| A (recommended): check only in development | if (ngDevMode && result instanceof AbstractControl) |
One line, measured. Development keeps the error, production doesn't |
B: check with a private helper from @angular/forms |
A symbol on AbstractControl and a private ɵisAbstractControl() export |
Keeps the production error. Adds a cross-package private API. Not measured |
| C: remove the check | Delete it | Smallest change, but it loses the diagnostic everywhere |
Where to look in the code:
FieldNodeContext.valueOfholds the check.- The triage note has the full measurements, the list of names that ship only because of the check, and the full code for each option.
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsNo status
Activity
perf(forms): avoid retaining AbstractControl in signal forms