Skip to content

Commit f4bfa84

Browse files
crisbetommalerba
authored andcommitted
perf(icon): avoid unnecessarily parsing icon sets
Some internal refactoring to improve the performance of the icon registry by avoiding unnecessary work: * When an icon set is registered as a string literal, we parse it immediately into an SVG element. This is inefficient since the set may not be required immediately. These changes make it so that we store the SVG text and we only parse it when it's required. * When an icon is requested, we try to find it in all any of the icon sets which requires us to parse them since we don't know which one is supposed to have it. These changes add an inexpensive `indexOf` check before parsing so that we can quickly rule out the sets that absolutely can't have the icon. The check won't exclude 100% of the unnecessary parsing, but it should help with the majority. Once this is in we could make it slightly smarter by doing something like `svgText.indexOf(`id="${iconName}"`)`, but that's a bit more risky. Fixes #18644.
1 parent 1921df3 commit f4bfa84

File tree

1 file changed

+52
-62
lines changed

1 file changed

+52
-62
lines changed

src/material/icon/icon-registry.ts

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -79,22 +79,17 @@ export interface IconOptions {
7979
* @docs-private
8080
*/
8181
class SvgIconConfig {
82-
url: SafeResourceUrl | null;
8382
svgElement: SVGElement | null;
8483

85-
constructor(url: SafeResourceUrl, options?: IconOptions);
86-
constructor(svgElement: SVGElement, options?: IconOptions);
87-
constructor(data: SafeResourceUrl | SVGElement, public options?: IconOptions) {
88-
// Note that we can't use `instanceof SVGElement` here,
89-
// because it'll break during server-side rendering.
90-
if (!!(data as any).nodeName) {
91-
this.svgElement = data as SVGElement;
92-
} else {
93-
this.url = data as SafeResourceUrl;
94-
}
95-
}
84+
constructor(
85+
public url: SafeResourceUrl,
86+
public svgText: string | null,
87+
public options?: IconOptions) {}
9688
}
9789

90+
/** Icon configuration whose content has already been loaded. */
91+
type LoadedSvgIconConfig = SvgIconConfig & {svgText: string};
92+
9893
/**
9994
* Service to register and display icons used by the `<mat-icon>` component.
10095
* - Registers icon URLs by namespace and name.
@@ -168,7 +163,7 @@ export class MatIconRegistry implements OnDestroy {
168163
*/
169164
addSvgIconInNamespace(namespace: string, iconName: string, url: SafeResourceUrl,
170165
options?: IconOptions): this {
171-
return this._addSvgIconConfig(namespace, iconName, new SvgIconConfig(url, options));
166+
return this._addSvgIconConfig(namespace, iconName, new SvgIconConfig(url, null, options));
172167
}
173168

174169
/**
@@ -179,14 +174,14 @@ export class MatIconRegistry implements OnDestroy {
179174
*/
180175
addSvgIconLiteralInNamespace(namespace: string, iconName: string, literal: SafeHtml,
181176
options?: IconOptions): this {
182-
const sanitizedLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
177+
const cleanLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
183178

184-
if (!sanitizedLiteral) {
179+
if (!cleanLiteral) {
185180
throw getMatIconFailedToSanitizeLiteralError(literal);
186181
}
187182

188-
const svgElement = this._createSvgElementForSingleIcon(sanitizedLiteral, options);
189-
return this._addSvgIconConfig(namespace, iconName, new SvgIconConfig(svgElement, options));
183+
return this._addSvgIconConfig(namespace, iconName,
184+
new SvgIconConfig('', cleanLiteral, options));
190185
}
191186

192187
/**
@@ -211,7 +206,7 @@ export class MatIconRegistry implements OnDestroy {
211206
* @param url
212207
*/
213208
addSvgIconSetInNamespace(namespace: string, url: SafeResourceUrl, options?: IconOptions): this {
214-
return this._addSvgIconSetConfig(namespace, new SvgIconConfig(url, options));
209+
return this._addSvgIconSetConfig(namespace, new SvgIconConfig(url, null, options));
215210
}
216211

217212
/**
@@ -221,14 +216,13 @@ export class MatIconRegistry implements OnDestroy {
221216
*/
222217
addSvgIconSetLiteralInNamespace(namespace: string, literal: SafeHtml,
223218
options?: IconOptions): this {
224-
const sanitizedLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
219+
const cleanLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
225220

226-
if (!sanitizedLiteral) {
221+
if (!cleanLiteral) {
227222
throw getMatIconFailedToSanitizeLiteralError(literal);
228223
}
229224

230-
const svgElement = this._svgElementFromString(sanitizedLiteral);
231-
return this._addSvgIconSetConfig(namespace, new SvgIconConfig(svgElement, options));
225+
return this._addSvgIconSetConfig(namespace, new SvgIconConfig('', cleanLiteral, options));
232226
}
233227

234228
/**
@@ -292,7 +286,7 @@ export class MatIconRegistry implements OnDestroy {
292286
return observableOf(cloneSvg(cachedIcon));
293287
}
294288

295-
return this._loadSvgIconFromConfig(new SvgIconConfig(safeUrl)).pipe(
289+
return this._loadSvgIconFromConfig(new SvgIconConfig(safeUrl, null)).pipe(
296290
tap(svg => this._cachedIconsByUrl.set(url!, svg)),
297291
map(svg => cloneSvg(svg)),
298292
);
@@ -335,15 +329,12 @@ export class MatIconRegistry implements OnDestroy {
335329
* Returns the cached icon for a SvgIconConfig if available, or fetches it from its URL if not.
336330
*/
337331
private _getSvgFromConfig(config: SvgIconConfig): Observable<SVGElement> {
338-
if (config.svgElement) {
332+
if (config.svgText) {
339333
// We already have the SVG element for this icon, return a copy.
340-
return observableOf(cloneSvg(config.svgElement));
334+
return observableOf(cloneSvg(this._svgElementFromConfig(config as LoadedSvgIconConfig)));
341335
} else {
342336
// Fetch the icon from the config's URL, cache it, and return a copy.
343-
return this._loadSvgIconFromConfig(config).pipe(
344-
tap(svg => config.svgElement = svg),
345-
map(svg => cloneSvg(svg)),
346-
);
337+
return this._loadSvgIconFromConfig(config).pipe(map(svg => cloneSvg(svg)));
347338
}
348339
}
349340

@@ -370,11 +361,11 @@ export class MatIconRegistry implements OnDestroy {
370361

371362
// Not found in any cached icon sets. If there are icon sets with URLs that we haven't
372363
// fetched, fetch them now and look for iconName in the results.
373-
const iconSetFetchRequests: Observable<SVGElement | null>[] = iconSetConfigs
374-
.filter(iconSetConfig => !iconSetConfig.svgElement)
364+
const iconSetFetchRequests: Observable<string | null>[] = iconSetConfigs
365+
.filter(iconSetConfig => !iconSetConfig.svgText)
375366
.map(iconSetConfig => {
376367
return this._loadSvgIconSetFromConfig(iconSetConfig).pipe(
377-
catchError((err: HttpErrorResponse): Observable<SVGElement | null> => {
368+
catchError((err: HttpErrorResponse) => {
378369
const url = this._sanitizer.sanitize(SecurityContext.RESOURCE_URL, iconSetConfig.url);
379370

380371
// Swallow errors fetching individual URLs so the
@@ -414,8 +405,14 @@ export class MatIconRegistry implements OnDestroy {
414405
// Iterate backwards, so icon sets added later have precedence.
415406
for (let i = iconSetConfigs.length - 1; i >= 0; i--) {
416407
const config = iconSetConfigs[i];
417-
if (config.svgElement) {
418-
const foundIcon = this._extractSvgIconFromSet(config.svgElement, iconName, config.options);
408+
409+
// Parsing the icon set's text into an SVG element can be expensive. We can avoid some of
410+
// the parsing by doing a quick check using `indexOf` to see if there's any chance for the
411+
// icon to be in the set. This won't be 100% accurate, but it should help us avoid at least
412+
// some of the parsing.
413+
if (config.svgText && config.svgText.indexOf(iconName) > -1) {
414+
const svg = this._svgElementFromConfig(config as LoadedSvgIconConfig);
415+
const foundIcon = this._extractSvgIconFromSet(svg, iconName, config.options);
419416
if (foundIcon) {
420417
return foundIcon;
421418
}
@@ -429,38 +426,22 @@ export class MatIconRegistry implements OnDestroy {
429426
* from it.
430427
*/
431428
private _loadSvgIconFromConfig(config: SvgIconConfig): Observable<SVGElement> {
432-
return this._fetchIcon(config)
433-
.pipe(map(svgText => this._createSvgElementForSingleIcon(svgText, config.options)));
429+
return this._fetchIcon(config).pipe(
430+
tap(svgText => config.svgText = svgText),
431+
map(() => this._svgElementFromConfig(config as LoadedSvgIconConfig))
432+
);
434433
}
435434

436435
/**
437-
* Loads the content of the icon set URL specified in the SvgIconConfig and creates an SVG element
438-
* from it.
436+
* Loads the content of the icon set URL specified in the
437+
* SvgIconConfig and attaches it to the config.
439438
*/
440-
private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable<SVGElement> {
441-
// If the SVG for this icon set has already been parsed, do nothing.
442-
if (config.svgElement) {
443-
return observableOf(config.svgElement);
439+
private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable<string | null> {
440+
if (config.svgText) {
441+
return observableOf(null);
444442
}
445443

446-
return this._fetchIcon(config).pipe(map(svgText => {
447-
// It is possible that the icon set was parsed and cached by an earlier request, so parsing
448-
// only needs to occur if the cache is yet unset.
449-
if (!config.svgElement) {
450-
config.svgElement = this._svgElementFromString(svgText);
451-
}
452-
453-
return config.svgElement;
454-
}));
455-
}
456-
457-
/**
458-
* Creates a DOM element from the given SVG string, and adds default attributes.
459-
*/
460-
private _createSvgElementForSingleIcon(responseText: string, options?: IconOptions): SVGElement {
461-
const svg = this._svgElementFromString(responseText);
462-
this._setSvgAttributes(svg, options);
463-
return svg;
444+
return this._fetchIcon(config).pipe(tap(svgText => config.svgText = svgText));
464445
}
465446

466447
/**
@@ -596,8 +577,6 @@ export class MatIconRegistry implements OnDestroy {
596577
return inProgressFetch;
597578
}
598579

599-
// TODO(jelbourn): for some reason, the `finalize` operator "loses" the generic type on the
600-
// Observable. Figure out why and fix it.
601580
const req = this._httpClient.get(url, {responseType: 'text', withCredentials}).pipe(
602581
finalize(() => this._inProgressUrlFetches.delete(url)),
603582
share(),
@@ -634,6 +613,17 @@ export class MatIconRegistry implements OnDestroy {
634613

635614
return this;
636615
}
616+
617+
/** Parses a config's text into an SVG element. */
618+
private _svgElementFromConfig(config: LoadedSvgIconConfig): SVGElement {
619+
if (!config.svgElement) {
620+
const svg = this._svgElementFromString(config.svgText);
621+
this._setSvgAttributes(svg, config.options);
622+
config.svgElement = svg;
623+
}
624+
625+
return config.svgElement;
626+
}
637627
}
638628

639629
/** @docs-private */

0 commit comments

Comments
 (0)