Skip to content

perf(forms): avoid creating unused field injectors - #71737

Merged
alxhub merged 1 commit into
angular:mainfrom
splincode:perf/signal-form-field-injectors
Nov 4, 2026
Merged

alxhub merged 1 commit into
angular:mainfrom
splincode:perf/signal-form-field-injectors

Conversation

@splincode

Copy link
Copy Markdown
Contributor

PR Checklist

  • The commit message follows the guidelines.
  • Tests for the changes have been added.
  • Docs have been added / updated (not needed: no public API change).

PR Type

  • Other: performance improvement

What is the current behavior?

Fields create an injector as soon as they have any metadata, even when every key is plain metadata. Built-in rules such as required(), min(), and debounce() therefore allocate injectors they never use. Removing a field without an injector also creates one just to destroy it.

Fixes #71623.

What is the new behavior?

Enter the field's injection context only for a managed key's create callback, and destroy only an existing field injector. Managed factories still run eagerly, in order, outside the reactive context, and share the same injector for a field.

Does this PR introduce a breaking change?

  • Yes
  • No

No public API or scheduling changes. Field removal still destroys managed metadata and cancels pending HTTP validation.

Validation

  • Signal Forms Node suite: 557 specs, 0 failures.
  • Signal Forms Chromium suite: 362 specs, 0 failures.
  • Two allocation regression tests fail on unmodified main.
  • Added coverage for plain metadata reactivity, built-in rules, managed-key injection and shared contexts, exactly-once removal cleanup, and pending HTTP validator cancellation.
  • ng-dev format, Prettier, and git diff --check pass.

Production-build check

An order-entry page renders 1,000 rows, with a required() SKU input and a min() quantity input in each row. Both optimized builds use the same application and instrumentation; one has these two changes transplanted into Angular Forms 22.2.3 (CLI/build 22.2.4). Node 24.22.1, Chromium 158.0.8117.0, seven alternating baseline/patched pairs in fresh browser contexts.

Measurement Before After
Field injectors created while rendering 2,000 inputs 2,000 0
Injectors created while removing the rows 1,000 0
Median additional live JS heap after GC 41,739,092 bytes 40,794,800 bytes

This saves about 0.90 MiB of live JS heap in that fixture. These are post-GC JS heap deltas, not peak memory or process RSS.

Median create-to-stable time was 259.8 / 254.4 ms; removal-to-stable was 60.8 / 56.3 ms. Samples overlap, so the concrete result is the avoided allocation and memory reduction, not a general latency guarantee. A separate 50-row control also preserved input count, editing/model propagation, validation, and row removal in all seven pairs. No page errors occurred.

The benchmark counts Injector.create calls using identical instrumentation in both builds. The application checks input rendering, required validation, edit propagation and successful removal. The installed Forms bundle was restored after building. The benchmark/support files are outside this PR.

@pullapprove
pullapprove Bot requested a review from crisbeto October 30, 2026 06:33
@angular-robot angular-robot Bot added area: performance Issues related to performance area: forms labels Oct 30, 2026
@ngbot ngbot Bot added this to the Backlog milestone Oct 30, 2026
untracked(() => {
for (const key of this.node.logicNode.logic.getMetadataKeys()) {
if (key.create) {
runInInjectionContext(this.node.structure.injector, () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a short comment that mention accessing the injector actually instantiates one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a short comment explaining that accessing structure.injector creates the field injector. Thanks!

@JeanMeche
JeanMeche removed the request for review from crisbeto October 30, 2026 13:26
@angular-robot angular-robot Bot added the area: docs Related to the documentation label Oct 30, 2026
@angular-robot
angular-robot Bot requested a review from JeanMeche October 30, 2026 13:46
Create field injectors only when managed metadata needs an injection
context, and destroy only injectors that have already been created.
Plain metadata and built-in synchronous rules otherwise allocate an
unused injector per field, including during removal.

Preserve eager managed metadata creation, the shared field injection
context, and teardown of asynchronous validators.

Fixes angular#71623
@splincode
splincode force-pushed the perf/signal-form-field-injectors branch from 0e0faaf to 8259122 Compare October 30, 2026 14:39
@JeanMeche JeanMeche added action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release labels Oct 30, 2026
@alxhub
alxhub merged commit 67e5069 into angular:main Nov 4, 2026
28 checks passed
@alxhub

alxhub commented Nov 4, 2026

Copy link
Copy Markdown
Member

This PR was merged into the repository. The changes were merged into the following branches:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: docs Related to the documentation area: forms area: performance Issues related to performance target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signal forms: required(), min(), and other built-in rules create an injector for every field

3 participants