From 9223fc3a12c85831cbe6730610eaf00a423a7432 Mon Sep 17 00:00:00 2001 From: Julian Lechner Date: Thu, 14 May 2026 15:27:36 +0200 Subject: [PATCH] improve: performance --- angular.json | 2 +- package-lock.json | 62 -------------------------- package.json | 4 -- src/app/app.config.ts | 4 +- src/app/home/home.component.html | 14 +++--- src/app/home/home.component.ts | 30 ++++++++----- src/app/imprint/imprint.component.html | 2 +- src/app/imprint/imprint.component.ts | 9 ++-- src/app/shared/icon/icon.component.ts | 47 +++++++++++++++++++ src/app/shared/icon/icon.types.ts | 6 +++ src/app/shared/icon/icons.ts | 41 +++++++++++++++++ src/main.server.ts | 8 +--- 12 files changed, 127 insertions(+), 102 deletions(-) create mode 100644 src/app/shared/icon/icon.component.ts create mode 100644 src/app/shared/icon/icon.types.ts create mode 100644 src/app/shared/icon/icons.ts diff --git a/angular.json b/angular.json index 599894c..792344a 100644 --- a/angular.json +++ b/angular.json @@ -20,7 +20,7 @@ "outputPath": "dist", "index": "src/index.html", "browser": "src/main.ts", - "polyfills": ["zone.js"], + "polyfills": [], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ diff --git a/package-lock.json b/package-lock.json index 6dd31aa..bac5275 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,10 +20,6 @@ "@angular/platform-server": "^21.2.13", "@angular/router": "^21.2.13", "@angular/ssr": "^21.2.11", - "@fortawesome/angular-fontawesome": "^4.0.0", - "@fortawesome/fontawesome-svg-core": "^7.2.0", - "@fortawesome/free-brands-svg-icons": "^7.2.0", - "@fortawesome/free-solid-svg-icons": "^7.2.0", "@types/markdown-it": "^14.1.2", "express": "^5.2.1", "markdown-it": "^14.1.1", @@ -4333,64 +4329,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fortawesome/angular-fontawesome": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-4.0.0.tgz", - "integrity": "sha512-TCqHqT5ovFY1A4RgMpoBUgS+RX3OVs39+CzHFgzDhbCPAopOa26J748TZJcuZwJAvGAk9tbWeVEmWuLByINAeg==", - "license": "MIT", - "dependencies": { - "@fortawesome/fontawesome-svg-core": "^7.1.0", - "tslib": "^2.8.1" - }, - "peerDependencies": { - "@angular/core": "^21.0.0" - } - }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", - "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", - "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", - "license": "MIT", - "dependencies": { - "@fortawesome/fontawesome-common-types": "7.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz", - "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "7.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", - "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "7.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@gar/promise-retry": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", diff --git a/package.json b/package.json index 938f15f..232a80b 100644 --- a/package.json +++ b/package.json @@ -66,10 +66,6 @@ "@angular/platform-server": "^21.2.13", "@angular/router": "^21.2.13", "@angular/ssr": "^21.2.11", - "@fortawesome/angular-fontawesome": "^4.0.0", - "@fortawesome/fontawesome-svg-core": "^7.2.0", - "@fortawesome/free-brands-svg-icons": "^7.2.0", - "@fortawesome/free-solid-svg-icons": "^7.2.0", "@types/markdown-it": "^14.1.2", "express": "^5.2.1", "markdown-it": "^14.1.1", diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 049bceb..c019ad9 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,12 +1,12 @@ import type { ApplicationConfig } from '@angular/core'; -import { provideZoneChangeDetection } from '@angular/core'; +import { provideZonelessChangeDetection } from '@angular/core'; import { provideRouter, withInMemoryScrolling } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), + provideZonelessChangeDetection(), provideRouter( routes, withInMemoryScrolling({ diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 55f146b..cd32c88 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -14,18 +14,16 @@

{{ content().home.contactTitle }}

@@ -90,7 +88,7 @@
@if (isLongBioMounted) {
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 66215b3..f345f35 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -8,10 +8,6 @@ import { signal, } from '@angular/core'; import { NgOptimizedImage } from '@angular/common'; -import { faArrowRight, faArrowDown, faEnvelope, faPhone } from '@fortawesome/free-solid-svg-icons'; -import { faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons'; -import type { IconDefinition } from '@fortawesome/angular-fontawesome'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { interval } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -23,11 +19,21 @@ import type { } from '../../languages/language.types'; import { FooterComponent } from '../footer/footer.component'; import { LanguageService } from '../services/language.service'; +import { IconComponent } from '../shared/icon/icon.component'; +import { + arrowDownIcon, + arrowRightIcon, + discordIcon, + envelopeIcon, + githubIcon, + linkedinIcon, + phoneIcon, +} from '../shared/icon/icons'; @Component({ selector: 'app-home', standalone: true, - imports: [FooterComponent, FaIconComponent, NgOptimizedImage], + imports: [FooterComponent, IconComponent, NgOptimizedImage], templateUrl: './home.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -65,13 +71,13 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { protected readonly global = global; protected readonly age = this.calculateAge(global.birthdate); protected readonly contactMailHref = `mailto:${global.contactMail}`; - protected readonly faArrowRight: IconDefinition = faArrowRight; - protected readonly faArrowDown: IconDefinition = faArrowDown; - protected readonly faEnvelope: IconDefinition = faEnvelope; - protected readonly faPhone: IconDefinition = faPhone; - protected readonly faDiscord: IconDefinition = faDiscord; - protected readonly faGithub: IconDefinition = faGithub; - protected readonly faLinkedin: IconDefinition = faLinkedin; + protected readonly arrowRightIcon = arrowRightIcon; + protected readonly arrowDownIcon = arrowDownIcon; + protected readonly envelopeIcon = envelopeIcon; + protected readonly phoneIcon = phoneIcon; + protected readonly discordIcon = discordIcon; + protected readonly githubIcon = githubIcon; + protected readonly linkedinIcon = linkedinIcon; private readonly isLongBioShownState = signal(false); private readonly isLongBioMountedState = signal(false); diff --git a/src/app/imprint/imprint.component.html b/src/app/imprint/imprint.component.html index ef7b6ab..1ebc11d 100644 --- a/src/app/imprint/imprint.component.html +++ b/src/app/imprint/imprint.component.html @@ -8,7 +8,7 @@
{{ content().imprint.backToPortfolio }}{{ content().imprint.backToPortfolio }}
diff --git a/src/app/imprint/imprint.component.ts b/src/app/imprint/imprint.component.ts index 70b6f8b..7469549 100644 --- a/src/app/imprint/imprint.component.ts +++ b/src/app/imprint/imprint.component.ts @@ -1,17 +1,16 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { RouterLink } from '@angular/router'; -import type { IconDefinition } from '@fortawesome/angular-fontawesome'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { global } from '../../global'; import { FooterComponent } from '../footer/footer.component'; import { LanguageService } from '../services/language.service'; +import { IconComponent } from '../shared/icon/icon.component'; +import { arrowLeftIcon } from '../shared/icon/icons'; @Component({ selector: 'app-imprint', standalone: true, - imports: [FooterComponent, FaIconComponent, RouterLink], + imports: [FooterComponent, IconComponent, RouterLink], templateUrl: './imprint.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -20,7 +19,7 @@ export class ImprintComponent { protected readonly content = this.languageService.content; protected readonly global = global; - protected readonly faArrowLeft: IconDefinition = faArrowLeft; + protected readonly arrowLeftIcon = arrowLeftIcon; protected readonly contactMailHref = `mailto:${global.contactMail}`; protected readonly abuseMailHref = `mailto:${global.abuseMail}`; diff --git a/src/app/shared/icon/icon.component.ts b/src/app/shared/icon/icon.component.ts new file mode 100644 index 0000000..ba86ac1 --- /dev/null +++ b/src/app/shared/icon/icon.component.ts @@ -0,0 +1,47 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import type { SvgIcon, SvgIconSize } from './icon.types'; + +@Component({ + selector: 'app-icon', + standalone: true, + template: ` + + `, + styles: [ + ` + :host { + display: inline-flex; + line-height: 1; + } + + .app-icon { + box-sizing: content-box; + display: inline-block; + width: 1.25em; + height: 1em; + overflow: visible; + vertical-align: -0.125em; + } + + .app-icon-lg { + font-size: 1.25em; + vertical-align: -0.2em; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class IconComponent { + readonly icon = input.required(); + readonly size = input('base'); +} diff --git a/src/app/shared/icon/icon.types.ts b/src/app/shared/icon/icon.types.ts new file mode 100644 index 0000000..c7b7dcf --- /dev/null +++ b/src/app/shared/icon/icon.types.ts @@ -0,0 +1,6 @@ +export interface SvgIcon { + readonly viewBox: string; + readonly path: string; +} + +export type SvgIconSize = 'base' | 'lg'; diff --git a/src/app/shared/icon/icons.ts b/src/app/shared/icon/icons.ts new file mode 100644 index 0000000..02400cb --- /dev/null +++ b/src/app/shared/icon/icons.ts @@ -0,0 +1,41 @@ +import type { SvgIcon } from './icon.types'; + +export const arrowDownIcon: SvgIcon = { + viewBox: '0 0 384 512', + path: 'M169.4 502.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 402.7 224 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 370.7-105.4-105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z', +}; + +export const arrowLeftIcon: SvgIcon = { + viewBox: '0 0 512 512', + path: 'M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 288 480 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-370.7 0 105.4-105.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z', +}; + +export const arrowRightIcon: SvgIcon = { + viewBox: '0 0 512 512', + path: 'M502.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L402.7 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l370.7 0-105.4 105.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z', +}; + +export const discordIcon: SvgIcon = { + viewBox: '0 0 576 512', + path: 'M492.5 69.8c-.2-.3-.4-.6-.8-.7-38.1-17.5-78.4-30-119.7-37.1-.4-.1-.8 0-1.1 .1s-.6 .4-.8 .8c-5.5 9.9-10.5 20.2-14.9 30.6-44.6-6.8-89.9-6.8-134.4 0-4.5-10.5-9.5-20.7-15.1-30.6-.2-.3-.5-.6-.8-.8s-.7-.2-1.1-.2c-41.3 7.1-81.6 19.6-119.7 37.1-.3 .1-.6 .4-.8 .7-76.2 113.8-97.1 224.9-86.9 334.5 0 .3 .1 .5 .2 .8s.3 .4 .5 .6c44.4 32.9 94 58 146.8 74.2 .4 .1 .8 .1 1.1 0s.7-.4 .9-.7c11.3-15.4 21.4-31.8 30-48.8 .1-.2 .2-.5 .2-.8s0-.5-.1-.8-.2-.5-.4-.6-.4-.3-.7-.4c-15.8-6.1-31.2-13.4-45.9-21.9-.3-.2-.5-.4-.7-.6s-.3-.6-.3-.9 0-.6 .2-.9 .3-.5 .6-.7c3.1-2.3 6.2-4.7 9.1-7.1 .3-.2 .6-.4 .9-.4s.7 0 1 .1c96.2 43.9 200.4 43.9 295.5 0 .3-.1 .7-.2 1-.2s.7 .2 .9 .4c2.9 2.4 6 4.9 9.1 7.2 .2 .2 .4 .4 .6 .7s.2 .6 .2 .9-.1 .6-.3 .9-.4 .5-.6 .6c-14.7 8.6-30 15.9-45.9 21.8-.2 .1-.5 .2-.7 .4s-.3 .4-.4 .7-.1 .5-.1 .8 .1 .5 .2 .8c8.8 17 18.8 33.3 30 48.8 .2 .3 .6 .6 .9 .7s.8 .1 1.1 0c52.9-16.2 102.6-41.3 147.1-74.2 .2-.2 .4-.4 .5-.6s.2-.5 .2-.8c12.3-126.8-20.5-236.9-86.9-334.5zm-302 267.7c-29 0-52.8-26.6-52.8-59.2s23.4-59.2 52.8-59.2c29.7 0 53.3 26.8 52.8 59.2 0 32.7-23.4 59.2-52.8 59.2zm195.4 0c-29 0-52.8-26.6-52.8-59.2s23.4-59.2 52.8-59.2c29.7 0 53.3 26.8 52.8 59.2 0 32.7-23.2 59.2-52.8 59.2z', +}; + +export const envelopeIcon: SvgIcon = { + viewBox: '0 0 512 512', + path: 'M48 64c-26.5 0-48 21.5-48 48 0 15.1 7.1 29.3 19.2 38.4l208 156c17.1 12.8 40.5 12.8 57.6 0l208-156c12.1-9.1 19.2-23.3 19.2-38.4 0-26.5-21.5-48-48-48L48 64zM0 196L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-188-198.4 148.8c-34.1 25.6-81.1 25.6-115.2 0L0 196z', +}; + +export const githubIcon: SvgIcon = { + viewBox: '0 0 512 512', + path: 'M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM252.8 8c-138.7 0-244.8 105.3-244.8 244 0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1 100-33.2 167.8-128.1 167.8-239 0-138.7-112.5-244-251.2-244zM105.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z', +}; + +export const linkedinIcon: SvgIcon = { + viewBox: '0 0 448 512', + path: 'M416 32L31.9 32C14.3 32 0 46.5 0 64.3L0 447.7C0 465.5 14.3 480 31.9 480L416 480c17.6 0 32-14.5 32-32.3l0-383.4C448 46.5 433.6 32 416 32zM135.4 416l-66.4 0 0-213.8 66.5 0 0 213.8-.1 0zM102.2 96a38.5 38.5 0 1 1 0 77 38.5 38.5 0 1 1 0-77zM384.3 416l-66.4 0 0-104c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9l0 105.8-66.4 0 0-213.8 63.7 0 0 29.2 .9 0c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9l0 117.2z', +}; + +export const phoneIcon: SvgIcon = { + viewBox: '0 0 512 512', + path: 'M160.2 25C152.3 6.1 131.7-3.9 112.1 1.4l-5.5 1.5c-64.6 17.6-119.8 80.2-103.7 156.4 37.1 175 174.8 312.7 349.8 349.8 76.3 16.2 138.8-39.1 156.4-103.7l1.5-5.5c5.4-19.7-4.7-40.3-23.5-48.1l-97.3-40.5c-16.5-6.9-35.6-2.1-47 11.8l-38.6 47.2C233.9 335.4 177.3 277 144.8 205.3L189 169.3c13.9-11.3 18.6-30.4 11.8-47L160.2 25z', +}; diff --git a/src/main.server.ts b/src/main.server.ts index b328231..b7c62b3 100644 --- a/src/main.server.ts +++ b/src/main.server.ts @@ -1,18 +1,12 @@ import type { BootstrapContext } from '@angular/platform-browser'; import { bootstrapApplication } from '@angular/platform-browser'; import type { ApplicationRef } from '@angular/core'; -import { provideZoneChangeDetection } from '@angular/core'; import { AppComponent } from './app/app.component'; import { config } from './app/app.config.server'; const bootstrap: (context: BootstrapContext) => Promise = ( context: BootstrapContext, -): Promise => - bootstrapApplication( - AppComponent, - { ...config, providers: [provideZoneChangeDetection(), ...config.providers] }, - context, - ); +): Promise => bootstrapApplication(AppComponent, config, context); export default bootstrap;