perf(core): avoid repeat searches for field directive

The `getControlDirective` is called multiple times, both at init and during each update run. Under the hood it performs a linear search for the `Field` directive.

We can speed this up by finding its index once and reusing it since the array of directive matches is static.
This commit is contained in:
Kristiyan Kostadinov 2025-11-19 17:00:18 +01:00 committed by Jessica Janiuk
parent 81ce1ba1d9
commit 5e6d8573f4
4 changed files with 47 additions and 52 deletions

View file

@ -7,6 +7,7 @@
*/
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {getClosureSafeProperty} from '../../util/property';
import {assertFirstCreatePass} from '../assert';
import {bindingUpdated} from '../bindings';
import {ɵCONTROL, ɵControl, ɵFieldState} from '../interfaces/control';
import {ComponentDef} from '../interfaces/definition';
@ -47,9 +48,12 @@ export function ɵɵcontrolCreate(): void {
const lView = getLView<{} | null>();
const tView = getTView();
const tNode = getCurrentTNode()!;
const control = tView.firstCreatePass
? getControlDirectiveFirstCreatePass(tView, tNode, lView)
: getControlDirective(tNode, lView);
if (tView.firstCreatePass) {
initializeControlFirstCreatePass(tView, tNode, lView);
}
const control = getControlDirective(tNode, lView);
if (!control) {
return;
@ -114,11 +118,9 @@ export function ɵɵcontrol<T>(value: T, sanitizer?: SanitizerFn | null): void {
const HAS_CONTROL_MASK = /* @__PURE__ */ (() =>
TNodeFlags.isNativeControl | TNodeFlags.isFormValueControl | TNodeFlags.isFormCheckboxControl)();
function getControlDirectiveFirstCreatePass<T>(
tView: TView,
tNode: TNode,
lView: LView,
): ɵControl<T> | undefined {
function initializeControlFirstCreatePass<T>(tView: TView, tNode: TNode, lView: LView): void {
ngDevMode && assertFirstCreatePass(tView);
const directiveIndices = tNode.inputs?.['field'];
if (!directiveIndices) {
// There are no matching inputs for the `[field]` property binding.
@ -137,13 +139,22 @@ function getControlDirectiveFirstCreatePass<T>(
}
// Search for the `ɵControl` directive.
const control = findControlDirective<T>(lView, directiveIndices);
if (!control) {
let controlIndex = -1;
for (let index of directiveIndices) {
if (ɵCONTROL in lView[index]) {
controlIndex = index;
break;
}
}
if (controlIndex === -1) {
// The `ɵControl` directive was not imported by this component.
return;
}
tNode.flags |= TNodeFlags.isFormControl;
const control = lView[controlIndex] as ɵControl<T>;
tNode.fieldIndex = controlIndex;
if (isComponentHost(tNode)) {
const componentDef = tView.data[componentIndex] as ComponentDef<unknown>;
@ -158,7 +169,7 @@ function getControlDirectiveFirstCreatePass<T>(
// Only check for an interop control if we haven't already found a custom one.
if (!(tNode.flags & HAS_CONTROL_MASK) && control.ɵinteropControl) {
tNode.flags |= TNodeFlags.isInteropControl;
return control;
return;
}
if (isNativeControl(tNode)) {
@ -172,7 +183,7 @@ function getControlDirectiveFirstCreatePass<T>(
}
if (tNode.flags & HAS_CONTROL_MASK) {
return control;
return;
}
const tagName = tNode.value;
@ -193,25 +204,9 @@ function getControlDirectiveFirstCreatePass<T>(
* @param tNode The `TNode` of the element to check.
* @param lView The `LView` that contains the element.
*/
function getControlDirective<T>(tNode: TNode, lView: LView): ɵControl<T> | undefined {
return tNode.flags & TNodeFlags.isFormControl
? findControlDirective(lView, tNode.inputs!['field'])
: undefined;
}
function findControlDirective<T>(
lView: LView,
directiveIndices: number[],
): ɵControl<T> | undefined {
for (let index of directiveIndices) {
const directive = lView[index];
if (ɵCONTROL in directive) {
return directive;
}
}
// The `Field` directive was not imported by this component.
return;
function getControlDirective<T>(tNode: TNode, lView: LView): ɵControl<T> | null {
const index = tNode.fieldIndex;
return index === -1 ? null : lView[index];
}
/** Returns whether the specified `componentDef` has a model input named `name`. */

View file

@ -172,55 +172,47 @@ export const enum TNodeFlags {
isInControlFlow = 1 << 9,
/**
* Bit #11 - This bit is set if the node represents a form control.
*
* True when the node has an input binding to a `ɵControl` directive (but not also to a custom
* component).
*/
isFormControl = 1 << 10,
/**
* Bit #12 - This bit is set if the node hosts a custom control component.
* Bit #11 - This bit is set if the node hosts a custom control component.
*
* A custom control component's model property is named `value`.
*/
isFormValueControl = 1 << 11,
isFormValueControl = 1 << 10,
/**
* Bit #13 - This bit is set if the node hosts a custom checkbox component.
* Bit #12 - This bit is set if the node hosts a custom checkbox component.
*
* A custom checkbox component's model property is named `checked`.
*/
isFormCheckboxControl = 1 << 12,
isFormCheckboxControl = 1 << 11,
/**
* Bit #14 - This bit is set if the node hosts an interoperable control implementation.
* Bit #13 - This bit is set if the node hosts an interoperable control implementation.
*
* This is used to bind to a `ControlValueAccessor` from `@angular/forms`.
*/
isInteropControl = 1 << 13,
isInteropControl = 1 << 12,
/**
* Bit #15 - This bit is set if the node is a native control.
* Bit #14 - This bit is set if the node is a native control.
*
* This is used to determine whether we can bind common control properties to the host element of
* a custom control when it doesn't define a corresponding input.
*/
isNativeControl = 1 << 14,
isNativeControl = 1 << 13,
/**
* Bit #16 - This bit is set if the node is a native control with a numeric type.
* Bit #15 - This bit is set if the node is a native control with a numeric type.
*
* This is used to determine whether the control supports the `min` and `max` properties.
*/
isNativeNumericControl = 1 << 15,
isNativeNumericControl = 1 << 14,
/**
* Bit #17 - This bit is set if the node is a native text control.
* Bit #16 - This bit is set if the node is a native text control.
*
* This is used to determine whether control supports the `minLength` and `maxLength` properties.
*/
isNativeTextControl = 1 << 16,
isNativeTextControl = 1 << 15,
}
/**
@ -381,6 +373,12 @@ export interface TNode {
*/
componentOffset: number;
/**
* Index at which the signal forms field directive is stored.
* Value is set to -1 if there are no field directives.
*/
fieldIndex: number;
/**
* Stores the last directive which had a styling instruction.
*

View file

@ -298,6 +298,7 @@ export function createTNode(
directiveEnd: -1,
directiveStylingLast: -1,
componentOffset: -1,
fieldIndex: -1,
propertyBindings: null,
flags,
providerIndexes: 0,

View file

@ -154,6 +154,7 @@ const ShapeOfTNode: ShapeOf<TNode> = {
directiveEnd: true,
directiveStylingLast: true,
componentOffset: true,
fieldIndex: true,
propertyBindings: true,
flags: true,
providerIndexes: true,