fix: performance
This commit is contained in:
@@ -37,7 +37,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
{ code: 'de', accent: 'DE' },
|
||||
];
|
||||
protected readonly dialogContent = computed(() =>
|
||||
this.languageService.getPack(this.selectedLanguage()),
|
||||
this.languageService.getShellPack(this.selectedLanguage()),
|
||||
);
|
||||
protected readonly languageSwitcherLabel = computed(() =>
|
||||
this.getLanguageLabel(this.currentLanguageCode(), this.content()),
|
||||
@@ -66,6 +66,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
protected confirmLanguage(): void {
|
||||
this.languageService.confirmLanguage(this.selectedLanguage());
|
||||
this.isLanguageSelectorOpen.set(false);
|
||||
this.kofiWidgetService.prioritizeLoad();
|
||||
}
|
||||
|
||||
protected reopenLanguageSelector(): void {
|
||||
@@ -80,6 +81,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.selectedLanguage.set(this.currentLanguageCode());
|
||||
this.isLanguageSelectorOpen.set(false);
|
||||
this.kofiWidgetService.prioritizeLoad();
|
||||
}
|
||||
|
||||
protected closeLanguageSelectorOnBackdrop(event: MouseEvent): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, inject, NgZone } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, NgZone } 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';
|
||||
@@ -8,9 +8,12 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { interval } from 'rxjs';
|
||||
|
||||
import { deLanguage } from '../../languages/de';
|
||||
import { enLanguage } from '../../languages/en';
|
||||
import { global } from '../../global';
|
||||
import type {
|
||||
LanguageBioTextEntry,
|
||||
LanguagePack,
|
||||
LanguagePortraitHighlight,
|
||||
LanguageProject,
|
||||
} from '../../languages/language.types';
|
||||
@@ -27,7 +30,9 @@ import { LanguageService } from '../services/language.service';
|
||||
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private readonly zone = inject(NgZone);
|
||||
private readonly languageService = inject(LanguageService);
|
||||
protected readonly content = this.languageService.content;
|
||||
protected readonly content = computed<LanguagePack>(() =>
|
||||
this.languageService.languageCode() === 'de' ? deLanguage : enLanguage,
|
||||
);
|
||||
|
||||
private scrollAnimationFrameId: number | null = null;
|
||||
private projectEntryAnimationFrameId: number | null = null;
|
||||
@@ -174,14 +179,27 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
protected scrollToContactLinks(): void {
|
||||
this.scrollToElementById('contact-links', this.getResponsiveScrollOffset(0.05));
|
||||
}
|
||||
|
||||
private shouldEnableScrollEffects(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
const isSmallViewport: boolean = window.innerWidth <= 700;
|
||||
const prefersReducedMotion: boolean = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)',
|
||||
).matches;
|
||||
const isCoarsePointer: boolean = window.matchMedia('(pointer: coarse)').matches;
|
||||
|
||||
return !isSmallViewport && !prefersReducedMotion && !isCoarsePointer;
|
||||
}
|
||||
|
||||
private initNameGradientScrollAnimation(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!this.shouldEnableScrollEffects()) return;
|
||||
window.addEventListener('scroll', this.scheduleNameGradientUpdate, { passive: true });
|
||||
window.addEventListener('resize', this.scheduleNameGradientUpdate, { passive: true });
|
||||
this.scheduleNameGradientUpdate();
|
||||
}
|
||||
private initProjectEntryRevealObserver(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!this.shouldEnableScrollEffects()) return;
|
||||
window.addEventListener('scroll', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
||||
window.addEventListener('resize', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
||||
this.scheduleProjectEntryRevealUpdate();
|
||||
@@ -199,7 +217,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
private initAboutSectionScrollAnimation(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!this.shouldEnableScrollEffects()) return;
|
||||
window.addEventListener('scroll', this.scheduleAboutSectionScrollAnimationUpdate, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, 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 { deLanguage } from '../../languages/de';
|
||||
import { enLanguage } from '../../languages/en';
|
||||
import { global } from '../../global';
|
||||
import type { LanguagePack } from '../../languages/language.types';
|
||||
import { FooterComponent } from '../footer/footer.component';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
|
||||
@@ -18,7 +21,9 @@ import { LanguageService } from '../services/language.service';
|
||||
export class ImprintComponent {
|
||||
private readonly languageService = inject(LanguageService);
|
||||
|
||||
protected readonly content = this.languageService.content;
|
||||
protected readonly content = computed<LanguagePack>(() =>
|
||||
this.languageService.languageCode() === 'de' ? deLanguage : enLanguage,
|
||||
);
|
||||
protected readonly global = global;
|
||||
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
|
||||
protected readonly contactMailHref = `mailto:${global.contactMail}`;
|
||||
|
||||
@@ -8,11 +8,14 @@ export class KofiWidgetService {
|
||||
|
||||
private readonly widgetScriptUrl = 'https://storage.ko-fi.com/cdn/scripts/overlay-widget.js';
|
||||
private readonly widgetIframeTitle = 'Ko-fi support chat';
|
||||
private readonly fallbackDelayMs = 10000;
|
||||
private readonly fallbackDelayMs = 2500;
|
||||
private readonly mobileBreakpointPx = 700;
|
||||
private readonly mobileViewportOffsetPx = 24;
|
||||
|
||||
private loadTimeoutId: number | null = null;
|
||||
private loadIdleCallbackId: number | null = null;
|
||||
private loadListenerAbortController: AbortController | null = null;
|
||||
private resizeListenerAbortController: AbortController | null = null;
|
||||
private iframeObserver: MutationObserver | null = null;
|
||||
private isWidgetScheduled = false;
|
||||
private isWidgetLoaded = false;
|
||||
@@ -85,10 +88,50 @@ export class KofiWidgetService {
|
||||
}
|
||||
}
|
||||
|
||||
teardown(): void {
|
||||
prioritizeLoad(): void {
|
||||
if (!isPlatformBrowser(this.platformId) || this.isWidgetLoaded || this.isWidgetDrawn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowRef = this.document.defaultView;
|
||||
if (windowRef === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearStartListeners();
|
||||
this.clearPendingLoadTimers();
|
||||
|
||||
const loadWidget = (): void => {
|
||||
this.loadIdleCallbackId = null;
|
||||
this.loadTimeoutId = null;
|
||||
void this.loadWidget();
|
||||
};
|
||||
|
||||
if (typeof windowRef.requestIdleCallback === 'function') {
|
||||
this.loadIdleCallbackId = windowRef.requestIdleCallback(loadWidget, { timeout: 1200 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadTimeoutId = windowRef.setTimeout(loadWidget, 200);
|
||||
}
|
||||
|
||||
teardown(): void {
|
||||
this.clearStartListeners();
|
||||
this.resizeListenerAbortController?.abort();
|
||||
this.resizeListenerAbortController = null;
|
||||
this.clearPendingLoadTimers();
|
||||
|
||||
this.iframeObserver?.disconnect();
|
||||
this.iframeObserver = null;
|
||||
}
|
||||
|
||||
private clearStartListeners(): void {
|
||||
this.loadListenerAbortController?.abort();
|
||||
this.loadListenerAbortController = null;
|
||||
}
|
||||
|
||||
private clearPendingLoadTimers(): void {
|
||||
const windowRef = this.document.defaultView;
|
||||
|
||||
if (windowRef !== null && this.loadTimeoutId !== null) {
|
||||
windowRef.clearTimeout(this.loadTimeoutId);
|
||||
@@ -103,14 +146,6 @@ export class KofiWidgetService {
|
||||
windowRef.cancelIdleCallback(this.loadIdleCallbackId);
|
||||
this.loadIdleCallbackId = null;
|
||||
}
|
||||
|
||||
this.iframeObserver?.disconnect();
|
||||
this.iframeObserver = null;
|
||||
}
|
||||
|
||||
private clearStartListeners(): void {
|
||||
this.loadListenerAbortController?.abort();
|
||||
this.loadListenerAbortController = null;
|
||||
}
|
||||
|
||||
private async loadWidget(): Promise<void> {
|
||||
@@ -190,7 +225,9 @@ export class KofiWidgetService {
|
||||
|
||||
this.isWidgetDrawn = true;
|
||||
this.ensureAccessibleIframeTitles();
|
||||
this.applyResponsiveWidgetLayout();
|
||||
this.observeInjectedIframes();
|
||||
this.observeViewportChanges();
|
||||
}
|
||||
|
||||
private ensureAccessibleIframeTitles(): void {
|
||||
@@ -201,6 +238,75 @@ export class KofiWidgetService {
|
||||
});
|
||||
}
|
||||
|
||||
private applyResponsiveWidgetLayout(): void {
|
||||
const windowRef = this.document.defaultView;
|
||||
if (windowRef === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMobileViewport: boolean = windowRef.innerWidth <= this.mobileBreakpointPx;
|
||||
const mobileWidthPx: number = Math.max(windowRef.innerWidth - this.mobileViewportOffsetPx, 280);
|
||||
const mobileHeightPx: number = Math.max(
|
||||
Math.min(Math.round(windowRef.innerHeight * 0.68), 520),
|
||||
360,
|
||||
);
|
||||
|
||||
this.document
|
||||
.querySelectorAll<HTMLElement>('[id*="kofi-widget-overlay"]')
|
||||
.forEach((overlay: HTMLElement): void => {
|
||||
if (isMobileViewport) {
|
||||
overlay.style.left = '12px';
|
||||
overlay.style.right = '12px';
|
||||
overlay.style.bottom = '12px';
|
||||
overlay.style.width = 'auto';
|
||||
overlay.style.maxWidth = `${mobileWidthPx}px`;
|
||||
} else {
|
||||
overlay.style.removeProperty('left');
|
||||
overlay.style.removeProperty('right');
|
||||
overlay.style.removeProperty('width');
|
||||
overlay.style.removeProperty('max-width');
|
||||
}
|
||||
});
|
||||
|
||||
this.document
|
||||
.querySelectorAll<HTMLElement>(
|
||||
'.floatingchat-container-wrap, .floatingchat-container-wrap-mobi',
|
||||
)
|
||||
.forEach((container: HTMLElement): void => {
|
||||
if (isMobileViewport) {
|
||||
container.style.width = `${mobileWidthPx}px`;
|
||||
container.style.maxWidth = 'calc(100vw - 24px)';
|
||||
container.style.left = '0';
|
||||
container.style.right = '0';
|
||||
} else {
|
||||
container.style.removeProperty('width');
|
||||
container.style.removeProperty('max-width');
|
||||
container.style.removeProperty('left');
|
||||
container.style.removeProperty('right');
|
||||
}
|
||||
});
|
||||
|
||||
this.document
|
||||
.querySelectorAll<HTMLIFrameElement>('iframe[id^="kofi-wo-container"]')
|
||||
.forEach((iframe: HTMLIFrameElement): void => {
|
||||
iframe.title = this.widgetIframeTitle;
|
||||
|
||||
if (isMobileViewport) {
|
||||
iframe.style.width = `${mobileWidthPx}px`;
|
||||
iframe.style.maxWidth = 'calc(100vw - 24px)';
|
||||
iframe.style.height = `${mobileHeightPx}px`;
|
||||
iframe.style.maxHeight = '68vh';
|
||||
iframe.style.borderRadius = '18px';
|
||||
} else {
|
||||
iframe.style.removeProperty('width');
|
||||
iframe.style.removeProperty('max-width');
|
||||
iframe.style.removeProperty('height');
|
||||
iframe.style.removeProperty('max-height');
|
||||
iframe.style.removeProperty('border-radius');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private observeInjectedIframes(): void {
|
||||
if (this.iframeObserver !== null) {
|
||||
return;
|
||||
@@ -208,6 +314,7 @@ export class KofiWidgetService {
|
||||
|
||||
this.iframeObserver = new MutationObserver((): void => {
|
||||
this.ensureAccessibleIframeTitles();
|
||||
this.applyResponsiveWidgetLayout();
|
||||
});
|
||||
|
||||
this.iframeObserver.observe(this.document.body, {
|
||||
@@ -215,4 +322,29 @@ export class KofiWidgetService {
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
private observeViewportChanges(): void {
|
||||
if (this.resizeListenerAbortController !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowRef = this.document.defaultView;
|
||||
if (windowRef === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resizeListenerAbortController = new AbortController();
|
||||
const listenerOptions: AddEventListenerOptions = {
|
||||
passive: true,
|
||||
signal: this.resizeListenerAbortController.signal,
|
||||
};
|
||||
|
||||
windowRef.addEventListener(
|
||||
'resize',
|
||||
(): void => {
|
||||
this.applyResponsiveWidgetLayout();
|
||||
},
|
||||
listenerOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
import { Injectable, computed, signal } from '@angular/core';
|
||||
|
||||
import { deLanguage } from '../../languages/de';
|
||||
import { enLanguage } from '../../languages/en';
|
||||
import type { LanguageCode, LanguagePack } from '../../languages/language.types';
|
||||
import type { LanguageCode, LanguageShellPack } from '../../languages/language.types';
|
||||
|
||||
const enShellLanguage: LanguageShellPack = {
|
||||
app: {
|
||||
selectorTitle: 'Select language',
|
||||
changeLanguage: 'Language',
|
||||
changeLanguageAriaLabel: 'Change language',
|
||||
closeSelector: 'Close language selection',
|
||||
languageEnglish: 'English',
|
||||
languageGerman: 'German',
|
||||
},
|
||||
footer: {
|
||||
noscriptMessage: 'Please enable JavaScript for the full experience.',
|
||||
imprintLink: 'Imprint',
|
||||
},
|
||||
};
|
||||
|
||||
const deShellLanguage: LanguageShellPack = {
|
||||
app: {
|
||||
selectorTitle: 'Sprache wählen',
|
||||
changeLanguage: 'Sprache',
|
||||
changeLanguageAriaLabel: 'Sprache wechseln',
|
||||
closeSelector: 'Sprachauswahl schliessen',
|
||||
languageEnglish: 'Englisch',
|
||||
languageGerman: 'Deutsch',
|
||||
},
|
||||
footer: {
|
||||
noscriptMessage: 'Bitte aktiviere JavaScript für die volle Darstellung.',
|
||||
imprintLink: 'Impressum',
|
||||
},
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LanguageService {
|
||||
@@ -12,7 +40,9 @@ export class LanguageService {
|
||||
|
||||
readonly languageCode = this.currentLanguageCode.asReadonly();
|
||||
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly();
|
||||
readonly content = computed<LanguagePack>(() => this.getPack(this.currentLanguageCode()));
|
||||
readonly content = computed<LanguageShellPack>(() =>
|
||||
this.getShellPack(this.currentLanguageCode()),
|
||||
);
|
||||
|
||||
initializeFromStorage(): LanguageCode {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -37,7 +67,7 @@ export class LanguageService {
|
||||
}
|
||||
}
|
||||
|
||||
getPack(code: LanguageCode): LanguagePack {
|
||||
return code === 'de' ? deLanguage : enLanguage;
|
||||
getShellPack(code: LanguageCode): LanguageShellPack {
|
||||
return code === 'de' ? deShellLanguage : enShellLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user