Skip to content

Commit c367c2c

Browse files
authored
perf(template-outlet): Improved performance (#16857)
* perf(template-outlet): Improved performance - **Eliminated duplicate cache lookups**: _getActionType() now returns both actionType and cachedView, avoiding redundant Map queries in _useCachedView() - **Cached indexOf() results in _moveView()**: Store indices instead of calling indexOf() twice for the same view references - **Optimized _recreateView()**: Reuse and update existing Map collections instead of creating new Map instances that overwrite existing entries - **Faster context operations**: Use Object.assign() for context updates and spread operator for cloning instead of manual iteration - **Improved _hasContextShapeChanged()**: Use Set.difference() with size check fast-path for O(1) lookups instead of O(n) indexOf()
1 parent 23f597e commit c367c2c

2 files changed

Lines changed: 86 additions & 80 deletions

File tree

projects/igniteui-angular/directives/src/directives/template-outlet/template_outlet.directive.ts

Lines changed: 85 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
import { Directive, EmbeddedViewRef, Input, OnChanges, ChangeDetectorRef, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef, NgZone, Output, EventEmitter, inject } from '@angular/core';
2-
1+
import {
2+
Directive,
3+
EmbeddedViewRef,
4+
Input,
5+
OnChanges,
6+
SimpleChange,
7+
SimpleChanges,
8+
TemplateRef,
9+
ViewContainerRef,
10+
Output,
11+
EventEmitter,
12+
inject
13+
} from '@angular/core';
314
import { IBaseEventArgs } from 'igniteui-angular/core';
415

516
/**
@@ -10,9 +21,17 @@ import { IBaseEventArgs } from 'igniteui-angular/core';
1021
standalone: true
1122
})
1223
export class IgxTemplateOutletDirective implements OnChanges {
13-
public _viewContainerRef = inject(ViewContainerRef);
14-
private _zone = inject(NgZone);
15-
public cdr = inject(ChangeDetectorRef);
24+
private readonly _viewContainerRef = inject(ViewContainerRef);
25+
26+
/**
27+
* The embedded views cache. Collection is key-value paired.
28+
* Key is the template type, value is another key-value paired collection
29+
* where the key is the template id and value is the embedded view for the related template.
30+
*/
31+
private readonly _embeddedViewsMap: Map<string, Map<any, EmbeddedViewRef<any>>> = new Map();
32+
33+
private _viewRef!: EmbeddedViewRef<any>;
34+
1635

1736
@Input() public igxTemplateOutletContext !: any;
1837

@@ -30,67 +49,68 @@ export class IgxTemplateOutletDirective implements OnChanges {
3049
@Output()
3150
public beforeViewDetach = new EventEmitter<IViewChangeEventArgs>();
3251

33-
private _viewRef !: EmbeddedViewRef<any>;
34-
35-
/**
36-
* The embedded views cache. Collection is key-value paired.
37-
* Key is the template type, value is another key-value paired collection
38-
* where the key is the template id and value is the embedded view for the related template.
39-
*/
40-
private _embeddedViewsMap: Map<string, Map<any, EmbeddedViewRef<any>>> = new Map();
41-
4252
public ngOnChanges(changes: SimpleChanges) {
43-
const actionType: TemplateOutletAction = this._getActionType(changes);
53+
const { actionType, cachedView } = this._getActionType(changes);
54+
4455
switch (actionType) {
4556
case TemplateOutletAction.CreateView: this._recreateView(); break;
4657
case TemplateOutletAction.MoveView: this._moveView(); break;
47-
case TemplateOutletAction.UseCachedView: this._useCachedView(); break;
58+
case TemplateOutletAction.UseCachedView: this._useCachedView(cachedView); break;
4859
case TemplateOutletAction.UpdateViewContext: this._updateExistingContext(this.igxTemplateOutletContext); break;
4960
}
5061
}
5162

52-
public cleanCache() {
53-
this._embeddedViewsMap.forEach((collection) => {
54-
collection.forEach((item => {
63+
public cleanCache(): void {
64+
for (const collection of this._embeddedViewsMap.values()) {
65+
for (const item of collection.values()) {
5566
if (!item.destroyed) {
5667
item.destroy();
5768
}
58-
}));
69+
}
5970
collection.clear();
60-
});
71+
}
72+
6173
this._embeddedViewsMap.clear();
6274
}
6375

64-
public cleanView(tmplID) {
65-
const embViewCollection = this._embeddedViewsMap.get(tmplID.type);
66-
const embView = embViewCollection?.get(tmplID.id);
67-
if (embView) {
68-
embView.destroy();
69-
this._embeddedViewsMap.get(tmplID.type).delete(tmplID.id);
76+
public cleanView(templateId: { type: string; id: any }): void {
77+
const viewCollection = this._embeddedViewsMap.get(templateId.type);
78+
const view = viewCollection?.get(templateId.id);
79+
80+
if (view) {
81+
view.destroy();
82+
this._embeddedViewsMap.get(templateId.type).delete(templateId.id);
7083
}
7184
}
7285

73-
private _recreateView() {
74-
const prevIndex = this._viewRef ? this._viewContainerRef.indexOf(this._viewRef) : -1;
86+
private _recreateView(): void {
87+
const prevIndex = this._viewContainerRef.indexOf(this._viewRef);
88+
7589
// detach old and create new
7690
if (prevIndex !== -1) {
7791
this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
7892
this._viewContainerRef.detach(prevIndex);
7993
}
94+
8095
if (this.igxTemplateOutlet) {
8196
this._viewRef = this._viewContainerRef.createEmbeddedView(
8297
this.igxTemplateOutlet, this.igxTemplateOutletContext);
8398
this.viewCreated.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
84-
const tmplId = this.igxTemplateOutletContext['templateID'];
85-
if (tmplId) {
99+
const templateId = this.igxTemplateOutletContext['templateID'];
100+
101+
if (templateId) {
86102
// if context contains a template id, check if we have a view for that template already stored in the cache
87103
// if not create a copy and add it to the cache in detached state.
88104
// Note: Views in detached state do not appear in the DOM, however they remain stored in memory.
89-
const resCollection = this._embeddedViewsMap.get(this.igxTemplateOutletContext['templateID'].type);
90-
const res = resCollection?.get(this.igxTemplateOutletContext['templateID'].id);
91-
if (!res) {
92-
this._embeddedViewsMap.set(this.igxTemplateOutletContext['templateID'].type,
93-
new Map([[this.igxTemplateOutletContext['templateID'].id, this._viewRef]]));
105+
let resCollection = this._embeddedViewsMap.get(templateId.type);
106+
107+
if (!resCollection) {
108+
resCollection = new Map();
109+
this._embeddedViewsMap.set(templateId.type, resCollection);
110+
}
111+
112+
if (!resCollection.has(templateId.id)) {
113+
resCollection.set(templateId.id, this._viewRef);
94114
}
95115
}
96116
}
@@ -100,16 +120,22 @@ export class IgxTemplateOutletDirective implements OnChanges {
100120
// using external view and inserting it in current view.
101121
const view = this.igxTemplateOutletContext['moveView'];
102122
const owner = this.igxTemplateOutletContext['owner'];
123+
103124
if (view !== this._viewRef) {
104-
if (owner._viewContainerRef.indexOf(view) !== -1) {
125+
const viewIndex = owner._viewContainerRef.indexOf(view);
126+
const viewRefIndex = this._viewContainerRef.indexOf(this._viewRef);
127+
128+
if (viewIndex !== -1) {
105129
// detach in case view it is attached somewhere else at the moment.
106130
this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
107-
owner._viewContainerRef.detach(owner._viewContainerRef.indexOf(view));
131+
owner._viewContainerRef.detach(viewIndex);
108132
}
109-
if (this._viewRef && this._viewContainerRef.indexOf(this._viewRef) !== -1) {
133+
134+
if (this._viewRef && viewRefIndex !== -1) {
110135
this.beforeViewDetach.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext });
111-
this._viewContainerRef.detach(this._viewContainerRef.indexOf(this._viewRef));
136+
this._viewContainerRef.detach(viewRefIndex);
112137
}
138+
113139
this._viewRef = view;
114140
this._viewContainerRef.insert(view, 0);
115141
this._updateExistingContext(this.igxTemplateOutletContext);
@@ -118,12 +144,9 @@ export class IgxTemplateOutletDirective implements OnChanges {
118144
this._updateExistingContext(this.igxTemplateOutletContext);
119145
}
120146
}
121-
private _useCachedView() {
147+
148+
private _useCachedView(cachedView: EmbeddedViewRef<any>) {
122149
// use view for specific template cached in the current template outlet
123-
const tmplID = this.igxTemplateOutletContext['templateID'];
124-
const cachedView = tmplID ?
125-
this._embeddedViewsMap.get(tmplID.type)?.get(tmplID.id) :
126-
null;
127150
// if view exists, but template has been changed and there is a view in the cache with the related template
128151
// then detach old view and insert the stored one with the matching template
129152
// after that update its context.
@@ -133,7 +156,7 @@ export class IgxTemplateOutletDirective implements OnChanges {
133156
}
134157

135158
this._viewRef = cachedView;
136-
const oldContext = this._cloneContext(cachedView.context);
159+
const oldContext = {...cachedView.context};
137160
this._viewContainerRef.insert(this._viewRef, 0);
138161
this._updateExistingContext(this.igxTemplateOutletContext);
139162
this.cachedViewLoaded.emit({ owner: this, view: this._viewRef, context: this.igxTemplateOutletContext, oldContext });
@@ -145,54 +168,41 @@ export class IgxTemplateOutletDirective implements OnChanges {
145168
}
146169

147170
private _hasContextShapeChanged(ctxChange: SimpleChange): boolean {
148-
const prevCtxKeys = Object.keys(ctxChange.previousValue || {});
149-
const currCtxKeys = Object.keys(ctxChange.currentValue || {});
171+
const prevKeys = new Set(Object.keys(ctxChange.previousValue || {}));
172+
const currKeys = new Set(Object.keys(ctxChange.currentValue || {}));
150173

151-
if (prevCtxKeys.length === currCtxKeys.length) {
152-
for (const propName of currCtxKeys) {
153-
if (prevCtxKeys.indexOf(propName) === -1) {
154-
return true;
155-
}
156-
}
157-
return false;
158-
} else {
174+
175+
if (prevKeys.size !== currKeys.size) {
159176
return true;
160177
}
161-
}
162178

163-
private _updateExistingContext(ctx: any): void {
164-
for (const propName of Object.keys(ctx)) {
165-
this._viewRef.context[propName] = this.igxTemplateOutletContext[propName];
166-
}
179+
return currKeys.difference(prevKeys).size > 0;
167180
}
168181

169-
private _cloneContext(ctx: any): any {
170-
const clone = {};
171-
for (const propName of Object.keys(ctx)) {
172-
clone[propName] = ctx[propName];
173-
}
174-
return clone;
182+
private _updateExistingContext(ctx: any): void {
183+
Object.assign(this._viewRef.context, ctx);
175184
}
176185

177-
private _getActionType(changes: SimpleChanges) {
186+
private _getActionType(changes: SimpleChanges): { actionType: TemplateOutletAction; cachedView: EmbeddedViewRef<any> | null } {
178187
const movedView = this.igxTemplateOutletContext['moveView'];
179-
const tmplID = this.igxTemplateOutletContext['templateID'];
180-
const cachedView = tmplID ?
181-
this._embeddedViewsMap.get(tmplID.type)?.get(tmplID.id) :
188+
const templateId = this.igxTemplateOutletContext['templateID'];
189+
const cachedView = templateId ?
190+
this._embeddedViewsMap.get(templateId.type)?.get(templateId.id) :
182191
null;
183192
const shouldRecreate = this._shouldRecreateView(changes);
193+
184194
if (movedView) {
185195
// view is moved from external source
186-
return TemplateOutletAction.MoveView;
196+
return { actionType: TemplateOutletAction.MoveView, cachedView };
187197
} else if (shouldRecreate && cachedView) {
188198
// should recreate (template or context change) and there is a matching template in cache
189-
return TemplateOutletAction.UseCachedView;
199+
return { actionType: TemplateOutletAction.UseCachedView, cachedView };
190200
} else if (!this._viewRef || shouldRecreate) {
191201
// no view or should recreate
192-
return TemplateOutletAction.CreateView;
202+
return { actionType: TemplateOutletAction.CreateView, cachedView };
193203
} else if (this.igxTemplateOutletContext) {
194204
// has context, update context
195-
return TemplateOutletAction.UpdateViewContext;
205+
return { actionType: TemplateOutletAction.UpdateViewContext, cachedView };
196206
}
197207
}
198208
}
@@ -212,8 +222,3 @@ export interface IViewChangeEventArgs extends IBaseEventArgs {
212222
export interface ICachedViewLoadedEventArgs extends IViewChangeEventArgs {
213223
oldContext: any;
214224
}
215-
216-
/**
217-
* @hidden
218-
*/
219-

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"forceConsistentCasingInFileNames": true,
1313
"esModuleInterop": true,
1414
"target": "ES2022",
15+
"lib": ["ESNext", "DOM"],
1516
"skipLibCheck": true,
1617
"typeRoots": [
1718
"node_modules/@types"

0 commit comments

Comments
 (0)