perf(core): move property remapping for dom properties to compiler (#62421)

Since we know that DOM properties won't go to an inputs, we can move the remapping logic to the compiler, saving us some processing on the client.

PR Close #62421
This commit is contained in:
Kristiyan Kostadinov
2025-07-02 14:04:44 +02:00
committed by Jessica Janiuk
parent c30f4eebc1
commit 8fbe558cbc
16 changed files with 118 additions and 36 deletions
@@ -12,7 +12,7 @@ template: function MyComponent_Template(rf, ctx) {
$r3$.ɵɵadvance(3);
$r3$.ɵɵattribute("aria-label", ctx.value1)("aria-label", ctx.value2);
$r3$.ɵɵadvance();
$r3$.ɵɵdomProperty("tabindex", ctx.value1)("tabindex", ctx.value2);
$r3$.ɵɵdomProperty("tabIndex", ctx.value1)("tabIndex", ctx.value2);
$r3$.ɵɵadvance();
$r3$.ɵɵclassMap(ctx.value2);
$r3$.ɵɵadvance();
@@ -1,7 +1,7 @@
hostBindings: function MyDirective_HostBindings(rf, ctx) {
…
if (rf & 2) {
$r3$.ɵɵdomProperty("tabindex", 1);
$r3$.ɵɵdomProperty("tabIndex", 1);
$r3$.ɵɵattribute("title", "my title")("id", "my-id");
}
}
@@ -1,6 +1,6 @@
hostBindings: function MyDirective_HostBindings(rf, ctx) {
…
if (rf & 2) {
$r3$.ɵɵdomProperty("title", ctx.myTitle)("tabindex", 1)("id", ctx.myId);
$r3$.ɵɵdomProperty("title", ctx.myTitle)("tabIndex", 1)("id", ctx.myId);
}
}
@@ -1,6 +1,6 @@
hostBindings: function MyDirective_HostBindings(rf, ctx) {
…
if (rf & 2) {
$r3$.ɵɵdomProperty("tabindex", 1)("title", ctx.myTitle)("id", ctx.myId);
$r3$.ɵɵdomProperty("tabIndex", 1)("title", ctx.myTitle)("id", ctx.myId);
}
}
@@ -1,13 +1,13 @@
hostBindings: function HostBindingDir_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵdomProperty("innerHtml", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute);
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute);
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle);
}
}
…
hostBindings: function HostBindingDir2_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵdomProperty("innerHtml", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrl)("src", ctx.evil)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute);
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrl)("src", ctx.evil)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute);
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle);
}
}
@@ -166,7 +166,7 @@ export declare class MyModule {
}
/****************************************************************************************************
* PARTIAL FILE: special_property_remapping.js
* PARTIAL FILE: special_property_remapping_property.js
****************************************************************************************************/
import { Component, NgModule } from '@angular/core';
import * as i0 from "@angular/core";
@@ -176,15 +176,13 @@ export class MyComponent {
}
}
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: false, selector: "my-component", ngImport: i0, template: `
<label [for]="forValue"></label>`, isInline: true });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: false, selector: "my-component", ngImport: i0, template: `<label [for]="forValue"></label>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
selector: 'my-component',
template: `
<label [for]="forValue"></label>`,
standalone: false
template: `<label [for]="forValue"></label>`,
standalone: false,
}]
}] });
export class MyModule {
@@ -198,7 +196,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
}] });
/****************************************************************************************************
* PARTIAL FILE: special_property_remapping.d.ts
* PARTIAL FILE: special_property_remapping_property.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyComponent {
@@ -212,6 +210,35 @@ export declare class MyModule {
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
}
/****************************************************************************************************
* PARTIAL FILE: special_property_remapping_dom_property.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class MyComponent {
constructor() {
this.forValue = 'some-input';
}
}
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `<label [for]="forValue"></label>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
template: `<label [for]="forValue"></label>`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: special_property_remapping_dom_property.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyComponent {
forValue: string;
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: temporary_variables.js
****************************************************************************************************/
@@ -44,15 +44,29 @@
]
},
{
"description": "should not remap property names whose names do not correspond to their attribute names",
"description": "should not remap special property names when outputting property instructions",
"inputFiles": [
"special_property_remapping.ts"
"special_property_remapping_property.ts"
],
"expectations": [
{
"failureMessage": "Incorrect template",
"files": [
"special_property_remapping.js"
"special_property_remapping_property.js"
]
}
]
},
{
"description": "should remap special property names when outputting domProperty instructions",
"inputFiles": [
"special_property_remapping_dom_property.ts"
],
"expectations": [
{
"failureMessage": "Incorrect template",
"files": [
"special_property_remapping_dom_property.js"
]
}
]
@@ -3,7 +3,7 @@ template: function MyComponent_Template(rf, ctx) {
$r3$.ɵɵdomElement(0, "div", 0)(1, "link", 1)(2, "div")(3, "img", 2)(4, "iframe", 3)(5, "a", 1)(6, "div");
}
if (rf & 2) {
$r3$.ɵɵdomProperty("innerHtml", ctx.evil, $r3$.ɵɵsanitizeHtml);
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml);
$r3$.ɵɵadvance();
$r3$.ɵɵdomProperty("href", ctx.evil, $r3$.ɵɵsanitizeResourceUrl);
$r3$.ɵɵadvance();
@@ -0,0 +1,13 @@
consts: [[__AttributeMarker.Bindings__, "for"]]
…
function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$i0$.ɵɵdomElement(0, "label", 0);
}
if (rf & 2) {
$i0$.ɵɵdomProperty("htmlFor", ctx.forValue);
}
}
@@ -0,0 +1,8 @@
import {Component} from '@angular/core';
@Component({
template: `<label [for]="forValue"></label>`,
})
export class MyComponent {
forValue = 'some-input';
}
@@ -1,7 +1,7 @@
consts: [[__AttributeMarker.Bindings__, "for"]]
// ...
…
function MyComponent_Template(rf, ctx) {
if (rf & 1) {
@@ -10,4 +10,4 @@ function MyComponent_Template(rf, ctx) {
if (rf & 2) {
$i0$.ɵɵproperty("for", ctx.forValue);
}
}
}
@@ -1,15 +1,13 @@
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<label [for]="forValue"></label>`,
standalone: false
selector: 'my-component',
template: `<label [for]="forValue"></label>`,
standalone: false,
})
export class MyComponent {
forValue = 'some-input';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {
}
export class MyModule {}
@@ -22,7 +22,7 @@ consts: () => {
}
if (rf & 2) {
$r3$.ɵɵadvance(3);
$r3$.ɵɵdomProperty("tabindex", ctx.tabIndex);
$r3$.ɵɵdomProperty("tabIndex", ctx.tabIndex);
$r3$.ɵɵadvance();
$r3$.ɵɵdomProperty("ngIf", ctx.cond);
$r3$.ɵɵadvance(2);
@@ -26,6 +26,19 @@ const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>([
['body', Identifiers.resolveBody],
]);
/**
* DOM properties that need to be remapped on the compiler side.
* Note: this mapping has to be kept in sync with the equally named mapping in the runtime.
*/
const DOM_PROPERTY_REMAPPING = new Map([
['class', 'className'],
['for', 'htmlFor'],
['formaction', 'formAction'],
['innerHtml', 'innerHTML'],
['readonly', 'readOnly'],
['tabindex', 'tabIndex'],
]);
/**
* Compiles semantic operations across all views and generates output `o.Statement`s with actual
* runtime calls in their place.
@@ -549,7 +562,12 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
ir.OpList.replace(
op,
unit.job.mode === TemplateCompilationMode.DomOnly && !op.isLegacyAnimationTrigger
? ng.domProperty(op.name, op.expression, op.sanitizer, op.sourceSpan)
? ng.domProperty(
DOM_PROPERTY_REMAPPING.get(op.name) ?? op.name,
op.expression,
op.sanitizer,
op.sourceSpan,
)
: ng.property(op.name, op.expression, op.sanitizer, op.sourceSpan),
);
break;
@@ -598,7 +616,12 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
} else {
ir.OpList.replace(
op,
ng.domProperty(op.name, op.expression, op.sanitizer, op.sourceSpan),
ng.domProperty(
DOM_PROPERTY_REMAPPING.get(op.name) ?? op.name,
op.expression,
op.sanitizer,
op.sourceSpan,
),
);
}
}
@@ -234,8 +234,7 @@ export function enableApplyRootElementTransformImpl() {
* object lookup) for performance reasons - the series of `if` checks seems to be the fastest way of
* mapping property names. Do NOT change without benchmarking.
*
* Note: this mapping has to be kept in sync with the equally named mapping in the template
* type-checking machinery of ngtsc.
* Note: this mapping has to be kept in sync with the equivalent mappings in the compiler.
*/
function mapPropName(name: string): string {
if (name === 'class') return 'className';
@@ -265,6 +264,11 @@ export function setPropertyAndInputs<T>(
return; // Stop propcessing if we've matched at least one input.
}
// If the property is going to a DOM node, we have to remap it.
if (tNode.type & TNodeType.AnyRNode) {
propName = mapPropName(propName);
}
setDomProperty(tNode, lView, propName, value, renderer, sanitizer);
}
@@ -287,7 +291,6 @@ export function setDomProperty<T>(
) {
if (tNode.type & TNodeType.AnyRNode) {
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
propName = mapPropName(propName);
if (ngDevMode) {
validateAgainstEventProperties(propName);
@@ -76,13 +76,11 @@ describe('property bindings', () => {
it('should bind to properties whose names do not correspond to their attribute names', () => {
@Component({
template: '<label [for]="forValue"></label>',
standalone: false,
})
class MyComp {
forValue?: string;
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
const labelNode = fixture.debugElement.query(By.css('label'));
@@ -104,7 +102,6 @@ describe('property bindings', () => {
@Component({
template: '',
selector: 'my-comp',
standalone: false,
})
class MyComp {
@Input() for!: string;
@@ -112,13 +109,12 @@ describe('property bindings', () => {
@Component({
template: '<my-comp [for]="forValue"></my-comp>',
standalone: false,
imports: [MyComp],
})
class App {
forValue?: string;
}
TestBed.configureTestingModule({declarations: [App, MyComp]});
const fixture = TestBed.createComponent(App);
const myCompNode = fixture.debugElement.query(By.directive(MyComp));
fixture.componentInstance.forValue = 'hello';