Skip to content

Commit 143dd01

Browse files
committed
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 9343d08 commit 143dd01

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
@@ -76,22 +76,17 @@ export interface IconOptions {
7676
* @docs-private
7777
*/
7878
class SvgIconConfig {
79-
url: SafeResourceUrl | null;
8079
svgElement: SVGElement | null;
8180

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

87+
/** Icon configuration whose content has already been loaded. */
88+
type LoadedSvgIconConfig = SvgIconConfig & {svgText: string};
89+
9590
/**
9691
* Service to register and display icons used by the `<mat-icon>` component.
9792
* - Registers icon URLs by namespace and name.
@@ -165,7 +160,7 @@ export class MatIconRegistry implements OnDestroy {
165160
*/
166161
addSvgIconInNamespace(namespace: string, iconName: string, url: SafeResourceUrl,
167162
options?: IconOptions): this {
168-
return this._addSvgIconConfig(namespace, iconName, new SvgIconConfig(url, options));
163+
return this._addSvgIconConfig(namespace, iconName, new SvgIconConfig(url, null, options));
169164
}
170165

171166
/**
@@ -176,14 +171,14 @@ export class MatIconRegistry implements OnDestroy {
176171
*/
177172
addSvgIconLiteralInNamespace(namespace: string, iconName: string, literal: SafeHtml,
178173
options?: IconOptions): this {
179-
const sanitizedLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
174+
const cleanLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
180175

181-
if (!sanitizedLiteral) {
176+
if (!cleanLiteral) {
182177
throw getMatIconFailedToSanitizeLiteralError(literal);
183178
}
184179

185-
const svgElement = this._createSvgElementForSingleIcon(sanitizedLiteral, options);
186-
return this._addSvgIconConfig(namespace, iconName, new SvgIconConfig(svgElement, options));
180+
return this._addSvgIconConfig(namespace, iconName,
181+
new SvgIconConfig('', cleanLiteral, options));
187182
}
188183

189184
/**
@@ -208,7 +203,7 @@ export class MatIconRegistry implements OnDestroy {
208203
* @param url
209204
*/
210205
addSvgIconSetInNamespace(namespace: string, url: SafeResourceUrl, options?: IconOptions): this {
211-
return this._addSvgIconSetConfig(namespace, new SvgIconConfig(url, options));
206+
return this._addSvgIconSetConfig(namespace, new SvgIconConfig(url, null, options));
212207
}
213208

214209
/**
@@ -218,14 +213,13 @@ export class MatIconRegistry implements OnDestroy {
218213
*/
219214
addSvgIconSetLiteralInNamespace(namespace: string, literal: SafeHtml,
220215
options?: IconOptions): this {
221-
const sanitizedLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
216+
const cleanLiteral = this._sanitizer.sanitize(SecurityContext.HTML, literal);
222217

223-
if (!sanitizedLiteral) {
218+
if (!cleanLiteral) {
224219
throw getMatIconFailedToSanitizeLiteralError(literal);
225220
}
226221

227-
const svgElement = this._svgElementFromString(sanitizedLiteral);
228-
return this._addSvgIconSetConfig(namespace, new SvgIconConfig(svgElement, options));
222+
return this._addSvgIconSetConfig(namespace, new SvgIconConfig('', cleanLiteral, options));
229223
}
230224

231225
/**
@@ -289,7 +283,7 @@ export class MatIconRegistry implements OnDestroy {
289283
return observableOf(cloneSvg(cachedIcon));
290284
}
291285

292-
return this._loadSvgIconFromConfig(new SvgIconConfig(safeUrl)).pipe(
286+
return this._loadSvgIconFromConfig(new SvgIconConfig(safeUrl, null)).pipe(
293287
tap(svg => this._cachedIconsByUrl.set(url!, svg)),
294288
map(svg => cloneSvg(svg)),
295289
);
@@ -332,15 +326,12 @@ export class MatIconRegistry implements OnDestroy {
332326
* Returns the cached icon for a SvgIconConfig if available, or fetches it from its URL if not.
333327
*/
334328
private _getSvgFromConfig(config: SvgIconConfig): Observable<SVGElement> {
335-
if (config.svgElement) {
329+
if (config.svgText) {
336330
// We already have the SVG element for this icon, return a copy.
337-
return observableOf(cloneSvg(config.svgElement));
331+
return observableOf(cloneSvg(this._svgElementFromConfig(config as LoadedSvgIconConfig)));
338332
} else {
339333
// Fetch the icon from the config's URL, cache it, and return a copy.
340-
return this._loadSvgIconFromConfig(config).pipe(
341-
tap(svg => config.svgElement = svg),
342-
map(svg => cloneSvg(svg)),
343-
);
334+
return this._loadSvgIconFromConfig(config).pipe(map(svg => cloneSvg(svg)));
344335
}
345336
}
346337

@@ -367,11 +358,11 @@ export class MatIconRegistry implements OnDestroy {
367358

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

377368
// Swallow errors fetching individual URLs so the
@@ -411,8 +402,14 @@ export class MatIconRegistry implements OnDestroy {
411402
// Iterate backwards, so icon sets added later have precedence.
412403
for (let i = iconSetConfigs.length - 1; i >= 0; i--) {
413404
const config = iconSetConfigs[i];
414-
if (config.svgElement) {
415-
const foundIcon = this._extractSvgIconFromSet(config.svgElement, iconName, config.options);
405+
406+
// Parsing the icon set's text into an SVG element can be expensive. We can avoid some of
407+
// the parsing by doing a quick check using `indexOf` to see if there's any chance for the
408+
// icon to be in the set. This won't be 100% accurate, but it should help us avoid at least
409+
// some of the parsing.
410+
if (config.svgText && config.svgText.indexOf(iconName) > -1) {
411+
const svg = this._svgElementFromConfig(config as LoadedSvgIconConfig);
412+
const foundIcon = this._extractSvgIconFromSet(svg, iconName, config.options);
416413
if (foundIcon) {
417414
return foundIcon;
418415
}
@@ -426,38 +423,22 @@ export class MatIconRegistry implements OnDestroy {
426423
* from it.
427424
*/
428425
private _loadSvgIconFromConfig(config: SvgIconConfig): Observable<SVGElement> {
429-
return this._fetchUrl(config.url)
430-
.pipe(map(svgText => this._createSvgElementForSingleIcon(svgText, config.options)));
426+
return this._fetchUrl(config.url).pipe(
427+
tap(svgText => config.svgText = svgText),
428+
map(() => this._svgElementFromConfig(config as LoadedSvgIconConfig))
429+
);
431430
}
432431

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

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

463444
/**
@@ -590,8 +571,6 @@ export class MatIconRegistry implements OnDestroy {
590571
return inProgressFetch;
591572
}
592573

593-
// TODO(jelbourn): for some reason, the `finalize` operator "loses" the generic type on the
594-
// Observable. Figure out why and fix it.
595574
const req = this._httpClient.get(url, {responseType: 'text'}).pipe(
596575
finalize(() => this._inProgressUrlFetches.delete(url)),
597576
share(),
@@ -628,6 +607,17 @@ export class MatIconRegistry implements OnDestroy {
628607

629608
return this;
630609
}
610+
611+
/** Parses a config's text into an SVG element. */
612+
private _svgElementFromConfig(config: LoadedSvgIconConfig): SVGElement {
613+
if (!config.svgElement) {
614+
const svg = this._svgElementFromString(config.svgText);
615+
this._setSvgAttributes(svg, config.options);
616+
config.svgElement = svg;
617+
}
618+
619+
return config.svgElement;
620+
}
631621
}
632622

633623
/** @docs-private */

0 commit comments

Comments
 (0)