improve: performance

This commit is contained in:
2026-05-14 15:36:37 +02:00
parent 9223fc3a12
commit d8d1528fa9
7 changed files with 115 additions and 95 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<div class="app-content" [attr.aria-hidden]="isLanguageSelectorOpen() ? 'true' : null" [attr.inert]="isLanguageSelectorOpen() ? '' : null"> <div class="app-content" [attr.aria-hidden]="isLanguageSelectorOpen() ? 'true' : null" [attr.inert]="isLanguageSelectorOpen() ? '' : null">
<router-outlet (activate)="scrollToTopAfterOutletActivation()"></router-outlet> <router-outlet></router-outlet>
</div> </div>
<button <button
+10 -55
View File
@@ -1,22 +1,18 @@
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
DestroyRef,
computed, computed,
effect, effect,
inject, inject,
PLATFORM_ID,
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import type { OnDestroy, OnInit } from '@angular/core'; import type { OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { filter } from 'rxjs';
import type { LanguageCode } from '../languages/language.types'; import type { LanguageCode } from '../languages/language.types';
import type { LanguageOption } from './app.types'; import type { LanguageOption } from './app.types';
import { LanguageService } from './services/language.service'; import { ShellLanguageService } from './services/shell-language.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -27,14 +23,11 @@ import { LanguageService } from './services/language.service';
}) })
export class AppComponent implements OnInit, OnDestroy { export class AppComponent implements OnInit, OnDestroy {
private readonly document = inject(DOCUMENT); private readonly document = inject(DOCUMENT);
private readonly destroyRef = inject(DestroyRef); private readonly shellLanguageService = inject(ShellLanguageService);
private readonly languageService = inject(LanguageService);
private readonly platformId = inject(PLATFORM_ID);
private readonly router = inject(Router);
protected readonly content = this.languageService.shellContent; protected readonly content = this.shellLanguageService.content;
protected readonly currentLanguageCode = this.languageService.languageCode; protected readonly currentLanguageCode = this.shellLanguageService.languageCode;
protected readonly isLanguageConfirmed = this.languageService.isLanguageConfirmed; protected readonly isLanguageConfirmed = this.shellLanguageService.isLanguageConfirmed;
protected readonly isLanguageSelectorOpen = signal(false); protected readonly isLanguageSelectorOpen = signal(false);
protected readonly selectedLanguage = signal<LanguageCode>('en'); protected readonly selectedLanguage = signal<LanguageCode>('en');
protected readonly languageOptions: readonly LanguageOption[] = [ protected readonly languageOptions: readonly LanguageOption[] = [
@@ -42,7 +35,7 @@ export class AppComponent implements OnInit, OnDestroy {
{ code: 'de', accent: 'DE' }, { code: 'de', accent: 'DE' },
]; ];
protected readonly dialogContent = computed(() => protected readonly dialogContent = computed(() =>
this.languageService.getShellPack(this.selectedLanguage()), this.shellLanguageService.getPack(this.selectedLanguage()),
); );
protected readonly languageSwitcherLabel = computed(() => protected readonly languageSwitcherLabel = computed(() =>
this.getLanguageLabel(this.currentLanguageCode(), this.content()), this.getLanguageLabel(this.currentLanguageCode(), this.content()),
@@ -54,17 +47,10 @@ export class AppComponent implements OnInit, OnDestroy {
this.document.body.style.overflow = overflowValue; this.document.body.style.overflow = overflowValue;
this.document.documentElement.style.overflow = overflowValue; this.document.documentElement.style.overflow = overflowValue;
}); });
this.router.events
.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((event: NavigationEnd): void => this.scrollToTopAfterRouteChange(event));
} }
ngOnInit(): void { ngOnInit(): void {
const initialLanguage = this.languageService.initializeFromStorage(); const initialLanguage = this.shellLanguageService.initializeFromStorage();
this.selectedLanguage.set(initialLanguage); this.selectedLanguage.set(initialLanguage);
} }
@@ -74,7 +60,7 @@ export class AppComponent implements OnInit, OnDestroy {
} }
protected confirmLanguage(): void { protected confirmLanguage(): void {
this.languageService.confirmLanguage(this.selectedLanguage()); this.shellLanguageService.confirmLanguage(this.selectedLanguage());
this.isLanguageSelectorOpen.set(false); this.isLanguageSelectorOpen.set(false);
} }
@@ -98,37 +84,6 @@ export class AppComponent implements OnInit, OnDestroy {
return language === 'de' ? content.app.languageGerman : content.app.languageEnglish; return language === 'de' ? content.app.languageGerman : content.app.languageEnglish;
} }
protected scrollToTopAfterOutletActivation(): void {
if (!isPlatformBrowser(this.platformId)) {
return;
}
this.scrollDocumentToTop();
this.document.defaultView?.requestAnimationFrame((): void => this.scrollDocumentToTop());
this.document.defaultView?.setTimeout((): void => this.scrollDocumentToTop(), 0);
this.document.defaultView?.setTimeout((): void => this.scrollDocumentToTop(), 100);
}
private scrollToTopAfterRouteChange(event: NavigationEnd): void {
if (!isPlatformBrowser(this.platformId) || event.urlAfterRedirects.includes('#')) {
return;
}
this.scrollToTopAfterOutletActivation();
}
private scrollDocumentToTop(): void {
const scrollingElement = this.document.scrollingElement ?? this.document.documentElement;
scrollingElement.scrollTop = 0;
scrollingElement.scrollLeft = 0;
this.document.documentElement.scrollTop = 0;
this.document.documentElement.scrollLeft = 0;
this.document.body.scrollTop = 0;
this.document.body.scrollLeft = 0;
this.document.defaultView?.scrollTo({ top: 0, left: 0, behavior: 'auto' });
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.document.body.style.overflow = ''; this.document.body.style.overflow = '';
this.document.documentElement.style.overflow = ''; this.document.documentElement.style.overflow = '';
+3 -3
View File
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { global } from '../../global'; import { global } from '../../global';
import { LanguageService } from '../services/language.service'; import { ShellLanguageService } from '../services/shell-language.service';
@Component({ @Component({
selector: 'app-footer', selector: 'app-footer',
@@ -11,9 +11,9 @@ import { LanguageService } from '../services/language.service';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FooterComponent { export class FooterComponent {
private readonly languageService = inject(LanguageService); private readonly shellLanguageService = inject(ShellLanguageService);
protected readonly content = this.languageService.shellContent; protected readonly content = this.shellLanguageService.content;
protected readonly global = global; protected readonly global = global;
protected readonly currentYear = new Date().getFullYear(); protected readonly currentYear = new Date().getFullYear();
} }
@@ -0,0 +1,35 @@
import { Injectable, signal } from '@angular/core';
import type { LanguageCode } from '../../languages/language.types';
@Injectable({ providedIn: 'root' })
export class LanguagePreferenceService {
private static readonly storageKey = 'portfolio-language';
private readonly currentLanguageCode = signal<LanguageCode>('en');
private readonly languageConfirmed = signal(true);
readonly languageCode = this.currentLanguageCode.asReadonly();
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly();
initializeFromStorage(): LanguageCode {
if (typeof window === 'undefined') {
return this.currentLanguageCode();
}
const storedLanguage = window.localStorage.getItem(LanguagePreferenceService.storageKey);
if (storedLanguage === 'de' || storedLanguage === 'en') {
this.currentLanguageCode.set(storedLanguage);
}
return this.currentLanguageCode();
}
confirmLanguage(code: LanguageCode): void {
this.currentLanguageCode.set(code);
this.languageConfirmed.set(true);
if (typeof window !== 'undefined') {
window.localStorage.setItem(LanguagePreferenceService.storageKey, code);
}
}
}
+9 -36
View File
@@ -1,54 +1,27 @@
import { Injectable, computed, signal } from '@angular/core'; import { Injectable, computed, inject } from '@angular/core';
import { deLanguage } from '../../languages/de'; import { deLanguage } from '../../languages/de';
import { enLanguage } from '../../languages/en'; import { enLanguage } from '../../languages/en';
import type { LanguageCode, LanguagePack, LanguageShellPack } from '../../languages/language.types'; import type { LanguageCode, LanguagePack } from '../../languages/language.types';
import { LanguagePreferenceService } from './language-preference.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class LanguageService { export class LanguageService {
private static readonly storageKey = 'portfolio-language'; private readonly languagePreferenceService = inject(LanguagePreferenceService);
private readonly currentLanguageCode = signal<LanguageCode>('en');
private readonly languageConfirmed = signal(true);
readonly languageCode = this.currentLanguageCode.asReadonly(); readonly languageCode = this.languagePreferenceService.languageCode;
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly(); readonly isLanguageConfirmed = this.languagePreferenceService.isLanguageConfirmed;
readonly content = computed<LanguagePack>(() => this.getPack(this.currentLanguageCode())); readonly content = computed<LanguagePack>(() => this.getPack(this.languageCode()));
readonly shellContent = computed<LanguageShellPack>(() => this.toShellPack(this.content()));
initializeFromStorage(): LanguageCode { initializeFromStorage(): LanguageCode {
if (typeof window === 'undefined') { return this.languagePreferenceService.initializeFromStorage();
return this.currentLanguageCode();
}
const storedLanguage = window.localStorage.getItem(LanguageService.storageKey);
if (storedLanguage === 'de' || storedLanguage === 'en') {
this.currentLanguageCode.set(storedLanguage);
}
return this.currentLanguageCode();
} }
confirmLanguage(code: LanguageCode): void { confirmLanguage(code: LanguageCode): void {
this.currentLanguageCode.set(code); this.languagePreferenceService.confirmLanguage(code);
this.languageConfirmed.set(true);
if (typeof window !== 'undefined') {
window.localStorage.setItem(LanguageService.storageKey, code);
}
}
getShellPack(code: LanguageCode): LanguageShellPack {
return this.toShellPack(this.getPack(code));
} }
getPack(code: LanguageCode): LanguagePack { getPack(code: LanguageCode): LanguagePack {
return code === 'de' ? deLanguage : enLanguage; return code === 'de' ? deLanguage : enLanguage;
} }
private toShellPack(pack: LanguagePack): LanguageShellPack {
return {
app: pack.app,
footer: pack.footer,
};
}
} }
@@ -0,0 +1,26 @@
import { Injectable, computed, inject } from '@angular/core';
import { deShellLanguage, enShellLanguage } from '../../languages/shell';
import type { LanguageCode, LanguageShellPack } from '../../languages/language.types';
import { LanguagePreferenceService } from './language-preference.service';
@Injectable({ providedIn: 'root' })
export class ShellLanguageService {
private readonly languagePreferenceService = inject(LanguagePreferenceService);
readonly languageCode = this.languagePreferenceService.languageCode;
readonly isLanguageConfirmed = this.languagePreferenceService.isLanguageConfirmed;
readonly content = computed<LanguageShellPack>(() => this.getPack(this.languageCode()));
initializeFromStorage(): LanguageCode {
return this.languagePreferenceService.initializeFromStorage();
}
confirmLanguage(code: LanguageCode): void {
this.languagePreferenceService.confirmLanguage(code);
}
getPack(code: LanguageCode): LanguageShellPack {
return code === 'de' ? deShellLanguage : enShellLanguage;
}
}
+31
View File
@@ -0,0 +1,31 @@
import type { LanguageShellPack } from './language.types';
export 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',
},
};
export const deShellLanguage: LanguageShellPack = {
app: {
selectorTitle: 'Sprache auswählen',
changeLanguage: 'Sprache',
changeLanguageAriaLabel: 'Sprache ändern',
closeSelector: 'Sprachauswahl schließen',
languageEnglish: 'Englisch',
languageGerman: 'Deutsch',
},
footer: {
noscriptMessage: 'Bitte aktiviere JavaScript um alles zu sehen.',
imprintLink: 'Impressum',
},
};