Issue
We have an Angular 16 Universal project and we want to find the best way to use SVG icons. Performance is crucial for our web app. We cannot use fonts such as Icomoon because the icons are multicolor, and it gets hard to customize and maintain.
First, we developed an Angular directive that inlines the icons in runtime. We tried the following modes:
Client-only: inlines the icons via a HttpClient.get() when the app runs in the browser. However, the icons do not start being downloaded until the whole main.js (which contains the directive) is loaded. This causes a perceptible flickering.
SSR + Client: with Angular hydration activated, the server performs the get calls to get the icons and the client wont repeat said calls. This solves the flickering, because the returned page already contains the SVGs. However, I am concerned about the creation of a bottleneck when introducing these requests in the server-side.
Moreover, the icons are currently served by an assets server, and we want to be able to send our components as a library to other teams so that they may reuse them. This could create problems if those teams make requests to our assets server from different hostnames (CORS). Therefore, some suggestions were made:
Inlining the SVGs on build time, specially for those that are critical and must be always shown. This would fix the potential issues of our directive, but it would increase the size of the scripts. Moreover, I haven't found a simple way to configure it via Webpack. Pasting them directly in our templates seems like an undesirable solution.
Using the assets folder so that the icons are included in the dist folder when passing our library to other teams.
Which would be the best way to include icons as SVG taking into account all these ideas?
Solution
Expanding on Eliseo's answer, you might consider a method to preload SVG icons during the application initialization, and then utilize an Angular component to display these icons inline, allowing for CSS customization.
That would provide an alternative approach to achieve inlining SVGs without needing to delve into Webpack configurations.
The structure of your Angular project would be:
src/
|-- app/
|-- components/
|-- header/
|-- header.component.html // Template file for the header component
|-- header.component.ts // TypeScript file for the header component
|-- header.component.css // CSS file for the header component
|-- core/
|-- services/
|-- svg.service.ts // Service for fetching and storing SVG icons
|-- shared/
|-- components/
|-- svg-icon/
|-- svg-icon.component.ts // Component for rendering SVG icons inline
|-- modules/
# other modules
|-- app.module.ts // Main application module where SVG_ICON_INITIALIZER is provided
|-- assets/
|-- symbol-defs.svg // SVG file containing symbol definitions for icons
|-- environments/
|-- environment.ts
# environment configuration files
|-- main.ts // Main entry file for the application
|-- index.html // Main HTML file
|-- styles.css // Global styles
|-- # other files
Create a service to handle fetching and storing SVG icons.
And use APP_INITIALIZER
to preload SVG icons during application initialization: that should mitigate the perceptible flickering mentioned in the "Client-only" mode and possibly alleviate the server bottleneck concern in the "SSR + Client" mode.
src/app/core/services/svg.service.ts
:
import { Injectable, APP_INITIALIZER, Provider } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class SvgService {
private svgDefs: string;
constructor(private http: HttpClient) {}
loadSvgIcons(): Promise<void> {
return this.http.get('assets/symbol-defs.svg', { responseType: 'text' })
.toPromise()
.then(svgDefs => {
this.svgDefs = svgDefs;
});
}
getIcon(iconId: string): string {
const match = this.svgDefs.match(new RegExp(`<symbol id="${iconId}"[^>]*>((.|\\n)*)<\\/symbol>`));
return match ? match[0] : '';
}
}
export const SVG_ICON_INITIALIZER: Provider = {
provide: APP_INITIALIZER,
useFactory: (svgService: SvgService) => () => svgService.loadSvgIcons(),
deps: [SvgService],
multi: true,
};
The loadSvgIcons
method fetches the SVG icons from the local asset file ('assets/symbol-defs.svg') and stores the SVG markup in the svgDefs
property. The getIcon(iconId: string)
method is used to retrieve specific SVG icon data from the svgDefs
property.
Then create an Angular component to render SVG icons inline.
src/shared/components/svg-icon/svg-icon.component.ts
:
import { Component, Input } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { SvgService } from './svg.service';
@Component({
selector: 'svg-icon',
template: `<div [innerHTML]="iconSvg"></div>`,
})
export class SvgIconComponent {
@Input() icon: string;
iconSvg: SafeHtml;
constructor(private svgService: SvgService, private sanitizer: DomSanitizer) {}
ngOnChanges() {
const iconSvgString = this.svgService.getIcon(this.icon);
this.iconSvg = this.sanitizer.bypassSecurityTrustHtml(iconSvgString);
}
}
The SvgIconComponent
is designed to render SVG icons inline. The icon
input property is used to specify the icon ID. The iconSvg
property is used to store the SVG icon string. The ngOnChanges
lifecycle hook is used to update the iconSvg
property when the icon
input property changes.
In your Angular module file (e.g., app.module.ts
), you would import the SVG_ICON_INITIALIZER
from the svg.service.ts
file and add it to the providers
array to make sure the SVG icons are preloaded during application initialization.
src/app/app.module.ts
:
// app.module.ts
import { SVG_ICON_INITIALIZER } from './svg.service';
@NgModule({
declarations: [SvgIconComponent],
imports: [/* */],
providers: [SVG_ICON_INITIALIZER],
bootstrap: [/* */]
})
export class AppModule { }
For illustration, in a typical Angular project, you might have a header
directory within a components
directory or directly under the app
directory, containing a header.component.html
file for the template of the header component.
In the header.component.html
file, you would use the svg-icon
component to render SVG icons inline:
src/app/components/header/header.component.html
(template file for the header component):
<div class="header">
<svg-icon icon="icon-1"></svg-icon>
<!-- other header content -->
</div>
Now that SVG icons are rendered inline, you can apply CSS styles as needed.
svg-icon svg {
fill: currentColor;
}
svg-icon:hover svg {
fill: gold;
}
You get:
+------------------------+ +-------------------------+ +-------------------+
| Angular Initialization | | SvgService | | SvgIconComponent |
| | | | | |
| APP_INITIALIZER | | loadSvgIcons() | | ngOnChanges() |
| (SVG_ICON_INITIALIZER) | ----> | getIcon(iconId: string) | ----> | render SVG inline |
| | | | | with [innerHTML] |
+------------------------+ +-------------------------+ +-------------------+
|
| Fetch SVG icons
v
+--------------------------+
| External Asset File |
| (assets/symbol-defs.svg) |
+--------------------------+
- The
APP_INITIALIZER
(usingSVG_ICON_INITIALIZER
) triggers theloadSvgIcons()
method inSvgService
during Angular's initialization phase. SvgService
fetches the SVG icons from the external asset file (assets/symbol-defs.svg
).SvgIconComponent
utilizesSvgService
to obtain specific SVG icon data via thegetIcon(iconId: string)
method whenever there is a change in theicon
input property (triggeringngOnChanges()
).SvgIconComponent
renders the SVG icon inline within its template using Angular's[innerHTML]
binding to inject the SVG markup into the DOM.
By encapsulating the SVG handling within a service and a component, that facilitates the reusability and sharing of components with other teams: you should be able to pass the library to other teams
Note: That does use a local asset file ('assets/symbol-defs.svg
') which is bundled with the application during the build process.
This is straightforward and avoids any network requests at runtime to fetch the SVG icons, which mitigates potential CORS issues. However, it does not leverage an external asset server.
In scenarios where the SVG icons are expected to change frequently or when there is a need to share icons across multiple projects, using an external asset server could be beneficial.
You would need to update the SvgService
to fetch the SVG icons from the asset server instead of the local 'assets/symbol-defs.svg
' file.
src/app/core/services/svg.service.ts
:
@Injectable({ providedIn: 'root' })
export class SvgService {
// previous code
loadSvgIcons(): Promise<void> {
// Update the URL to point to the asset server
return this.http.get('https://assets-server.com/symbol-defs.svg', { responseType: 'text' })
.toPromise()
.then(svgDefs => {
this.svgDefs = svgDefs;
});
}
// rest of the code
}
But you will also need to handle CORS issues that may arise when fetching the SVG icons from an external asset server.
Make sure the asset server is configured to allow cross-origin requests from the domains where your Angular applications are hosted. That can typically be done by setting appropriate CORS headers on the asset server.
// Example CORS headers on the asset server
Access-Control-Allow-Origin: https://your-angular-app.com
Access-Control-Allow-Methods: GET, OPTIONS
See more at "Angular CORS Guide: Fixing errors", from Saujan Ghimire
You might also need to implement versioning and cache-busting strategies to make sure the latest version of the SVG icons are fetched from the asset server whenever there are updates. That can be done by appending a version query parameter to the URL when fetching the SVG icons.
The version
variable can be hardcoded to 'v1
'. Whenever there is an update to the SVG icons, you would update this value to a new version string, e.g., 'v2
', 'v3
', and so forth. That change in the URL triggers the browser to fetch the updated SVG icons from the server, bypassing any cached version.
src/environments/environment.ts
:
// src/environments/environment.ts
export const environment = {
production: false,
SVG_ICON_VERSION: 'v1',
// other environment-specific configurations
};
By placing the SVG_ICON_VERSION
variable in the environment file, you allow for environment-specific versioning of your SVG icons. For example, you might have a different version of the SVG icons in your development environment compared to your production environment.
Then you would update the loadSvgIcons()
method in the SvgService
to append the version query parameter to the URL when fetching the SVG icons.
src/app/core/services/svg.service.ts
:
import { environment } from '../../../environments/environment';
@Injectable({ providedIn: 'root' })
export class SvgService {
// previous code
loadSvgIcons(): Promise<void> {
const version = 'v1'; // Update this value whenever the SVG icons are updated
return this.http.get(`https://assets-server.com/symbol-defs.svg?version=${version}`, { responseType: 'text' })
.toPromise()
.then(svgDefs => {
this.svgDefs = svgDefs;
});
}
// rest of the code
}
That would leverage an external asset server to serve SVG icons, facilitating easier updates and sharing of icons across projects. However, it does introduce a network request at runtime to fetch the SVG icons, which could potentially introduce a bottleneck.
+--------------------+ +-----------------+ +--------------------+
| Angular | | SvgService | | External Asset |
| Application | | | | Server |
| | | loadSvgIcons() | | |
| 1. Bootstraps | | 1. Fetches SVG | | 1. Serves SVG |
| 2. Calls | | icons from | | icons |
| APP_INITIALIZER | -------> | external server | -------> | 2. Checks CORS |
| (loadSvgIcons) | | with version | | headers |
| 3. Renders | | query param | | 3. Returns SVG |
| components | | 2. Stores SVG | | icons |
| with SVG icons | <------ | icons | <------- | |
+--------------------+ +-----------------+ +--------------------+
- The Angular application bootstraps and calls the
APP_INITIALIZER
(which triggersloadSvgIcons
method inSvgService
). SvgService
sends a request to fetch SVG icons from the external asset server, appending a version query parameter to the URL.- The external asset server serves the SVG icons, after checking the CORS headers to ensure the request is coming from an allowed domain.
SvgService
stores the fetched SVG icons for later use.- The Angular application renders components, which use the stored SVG icons from
SvgService
for displaying SVG icons inline.
I'm studying the
APP_INITIALIZER
option, but if I'm right, untilmain.js
is completely loaded, the code won't be executed at all.
That would still cause some perceptible flickering, especially if themain.js
file keeps growing in the future."
True, the APP_INITIALIZER
token is used to execute functions when an application starts, but it does run after the main.js
file is loaded.
If main.js
becomes large and takes a significant amount of time to load, there would still be a perceptible delay before the SVG icons are fetched and displayed, potentially leading to flickering.
To mitigate this, you have serval approaches:
You can try and optimize the bundle size: reducing the size of the
main.js
bundle would result in faster loading times. That can be achieved through code-splitting, tree-shaking, and other bundle optimization techniques.With server-side rendering, the SVG icons could be fetched and inlined into the HTML on the server before it is sent to the client. That would eliminate the flickering since the icons would already be present on the initial render.
Another approach would be to implement a service worker to cache the SVG icons after the first load can eliminate network latency in subsequent loads. The service worker could preload essential assets, including SVG icons, ensuring they are available as soon as the app loads.
If the SVG icons are hosted on a Content Delivery Network (CDN), they could be loaded more quickly than from a single server, especially if the CDN is optimized for delivering static assets.
HTTP/2 or HTTP/3 protocols can also help in loading assets in parallel, reducing the overall load time.
Answered By - VonC
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.