Revert "fix: performance"

This reverts commit cc39abed41.
This commit is contained in:
2026-04-24 18:19:54 +02:00
parent cc39abed41
commit c52856aaba
58 changed files with 140 additions and 195 deletions
+4 -14
View File
@@ -26,13 +26,8 @@
"assets": [ "assets": [
{ {
"glob": "**/*", "glob": "**/*",
"input": "src/assets/optimized", "input": "src/assets",
"output": "assets/optimized" "output": "assets"
},
{
"glob": "**/*",
"input": "src/assets/logos",
"output": "assets/logos"
}, },
{ {
"glob": "**/*", "glob": "**/*",
@@ -113,13 +108,8 @@
"assets": [ "assets": [
{ {
"glob": "**/*", "glob": "**/*",
"input": "src/assets/optimized", "input": "src/assets",
"output": "assets/optimized" "output": "assets"
},
{
"glob": "**/*",
"input": "src/assets/logos",
"output": "assets/logos"
}, },
{ {
"glob": "**/*", "glob": "**/*",
+1 -2
View File
@@ -24,11 +24,10 @@
"check": "npm run build && npm run lint && npm run format:check && npm run version:check && npm run text:check", "check": "npm run build && npm run lint && npm run format:check && npm run version:check && npm run text:check",
"fix": "npm run version:place && npm run lint:fix && npm run text:fix && npm run format", "fix": "npm run version:place && npm run lint:fix && npm run text:fix && npm run format",
"------ BUILD": "", "------ BUILD": "",
"clean": "del-cli dist",
"generate:static-files": "node scripts/generate-static-files.cjs", "generate:static-files": "node scripts/generate-static-files.cjs",
"generate:assets": "npm run generate:static-files", "generate:assets": "npm run generate:static-files",
"compress:assets": "node scripts/compress-assets.cjs", "compress:assets": "node scripts/compress-assets.cjs",
"build": "npm run clean && npm run generate:assets && ng build --configuration production && npm run compress:assets && node scripts/copy-static-root-files.cjs", "build": "npm run generate:assets && ng build --configuration production && npm run compress:assets && node scripts/copy-static-root-files.cjs",
"------ STARTUP": "", "------ STARTUP": "",
"test": "ng test", "test": "ng test",
"dev": "npm run generate:assets && ng serve", "dev": "npm run generate:assets && ng serve",
+3 -2
View File
@@ -1,7 +1,7 @@
import type { ApplicationConfig } from '@angular/core'; import type { ApplicationConfig } from '@angular/core';
import { provideZoneChangeDetection } from '@angular/core'; import { importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withInMemoryScrolling } from '@angular/router'; import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { BrowserModule, provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { routes } from './app.routes'; import { routes } from './app.routes';
@@ -16,5 +16,6 @@ export const appConfig: ApplicationConfig = {
}), }),
), ),
provideClientHydration(withEventReplay()), provideClientHydration(withEventReplay()),
importProvidersFrom(BrowserModule),
], ],
}; };
+5 -12
View File
@@ -1,17 +1,10 @@
import type { Routes } from '@angular/router'; import type { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ImprintComponent } from './imprint/imprint.component';
export const routes: Routes = [ export const routes: Routes = [
{ { path: '', component: HomeComponent, pathMatch: 'full' },
path: '', { path: 'imprint', component: ImprintComponent, pathMatch: 'full' },
loadComponent: async () =>
(await import('./home/home.component')).HomeComponent,
pathMatch: 'full',
},
{
path: 'imprint',
loadComponent: async () =>
(await import('./imprint/imprint.component')).ImprintComponent,
pathMatch: 'full',
},
{ path: '**', redirectTo: '' }, { path: '**', redirectTo: '' },
]; ];
+3 -22
View File
@@ -64,28 +64,13 @@
[title]="portraitHighlights.length > 1 ? 'Show next image' : 'Single image'" [title]="portraitHighlights.length > 1 ? 'Show next image' : 'Single image'"
> >
<div class="portrait-media" [class.portrait-media-switching]="isPortraitSwitching"> <div class="portrait-media" [class.portrait-media-switching]="isPortraitSwitching">
<picture>
<source
type="image/avif"
[attr.srcset]="currentPortraitHighlight.avifSrcset"
sizes="(max-width: 640px) calc(100vw - 2rem), (max-width: 900px) min(560px, calc(100vw - 2rem)), 560px"
/>
<source
type="image/webp"
[attr.srcset]="currentPortraitHighlight.webpSrcset"
sizes="(max-width: 640px) calc(100vw - 2rem), (max-width: 900px) min(560px, calc(100vw - 2rem)), 560px"
/>
<img <img
class="hero-feature-image" class="hero-feature-image"
[src]="currentPortraitHighlight.image" [src]="currentPortraitHighlight.image"
[attr.width]="currentPortraitHighlight.width" width="560"
[attr.height]="currentPortraitHighlight.height" height="420"
[alt]="currentPortraitHighlight.alt" [alt]="global.firstname + ' highlight'"
fetchpriority="high"
loading="eager"
decoding="async"
/> />
</picture>
@if (currentPortraitHighlightIndex === 0) { @if (currentPortraitHighlightIndex === 0) {
<span class="portrait-click-hint">Click me!</span> <span class="portrait-click-hint">Click me!</span>
} }
@@ -163,11 +148,7 @@
class="project-icon" class="project-icon"
[class.project-icon-circle]="project.CircleIcon !== false" [class.project-icon-circle]="project.CircleIcon !== false"
[src]="project.icon" [src]="project.icon"
[attr.width]="project.iconWidth ?? 108"
[attr.height]="project.iconHeight ?? 108"
[alt]="project.title + ' icon'" [alt]="project.title + ' icon'"
loading="lazy"
decoding="async"
/> />
} }
+74 -6
View File
@@ -22,6 +22,7 @@ import { FooterComponent } from '../footer/footer.component';
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly sanitizer = inject(DomSanitizer); private readonly sanitizer = inject(DomSanitizer);
private readonly zone = inject(NgZone); private readonly zone = inject(NgZone);
private preloadedImages: HTMLImageElement[] = [];
private tallestPortraitAspectRatio: number | null = null; private tallestPortraitAspectRatio: number | null = null;
private lockedHeroSectionMinHeight: number = 0; private lockedHeroSectionMinHeight: number = 0;
private projectEntryObserver: IntersectionObserver | null = null; private projectEntryObserver: IntersectionObserver | null = null;
@@ -81,7 +82,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.initPortraitAspectRatio(); this.preloadImageAssets();
this.initNameGradientScrollAnimation(); this.initNameGradientScrollAnimation();
this.zone.runOutsideAngular((): void => { this.zone.runOutsideAngular((): void => {
@@ -180,14 +181,61 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
this.scrollToElementById('contact-links', this.getResponsiveScrollOffset(0.05)); this.scrollToElementById('contact-links', this.getResponsiveScrollOffset(0.05));
} }
private initPortraitAspectRatio(): void { private preloadImageAssets(): void {
const minAspectRatio: number = this.portraitHighlights.reduce( if (typeof window === 'undefined') {
(minRatio: number, highlight: PortraitHighlight): number => { return;
if (highlight.width <= 0 || highlight.height <= 0) { }
const projectIcons: string[] = this.projects
.map((project: PortfolioProject): string | undefined => project.icon)
.filter((icon: string | undefined): icon is string => Boolean(icon));
const uniqueImageUrls: string[] = Array.from(
new Set<string>([
...this.portraitHighlights.map((highlight: PortraitHighlight): string => highlight.image),
...projectIcons,
]),
);
this.preloadedImages = uniqueImageUrls.map((url: string): HTMLImageElement => {
const image = new Image();
image.decoding = 'async';
image.loading = 'eager';
image.src = url;
return image;
});
this.syncHeroHeightToLargestPortraitImage();
}
private syncHeroHeightToLargestPortraitImage(): void {
if (typeof window === 'undefined') {
return;
}
const portraitImageUrls: Set<string> = new Set<string>(
this.portraitHighlights.map(
(highlight: PortraitHighlight): string =>
new URL(highlight.image, window.location.href).href,
),
);
const portraitImages: HTMLImageElement[] = this.preloadedImages.filter(
(image: HTMLImageElement): boolean => portraitImageUrls.has(image.src),
);
if (portraitImages.length === 0) {
return;
}
const finalizeAspectRatio = (): void => {
const minAspectRatio: number = portraitImages.reduce(
(minRatio: number, image: HTMLImageElement): number => {
if (image.naturalWidth === 0 || image.naturalHeight === 0) {
return minRatio; return minRatio;
} }
const currentRatio: number = highlight.width / highlight.height; const currentRatio: number = image.naturalWidth / image.naturalHeight;
return minRatio === 0 ? currentRatio : Math.min(minRatio, currentRatio); return minRatio === 0 ? currentRatio : Math.min(minRatio, currentRatio);
}, },
0, 0,
@@ -195,6 +243,26 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
if (minAspectRatio > 0) { if (minAspectRatio > 0) {
this.tallestPortraitAspectRatio = minAspectRatio; this.tallestPortraitAspectRatio = minAspectRatio;
this.scheduleHeroSectionHeightUpdate();
}
};
let pendingLoads: number = portraitImages.length;
const onImageSettled = (): void => {
pendingLoads -= 1;
if (pendingLoads === 0) {
finalizeAspectRatio();
}
};
for (const image of portraitImages) {
if (image.complete) {
onImageSettled();
continue;
}
image.addEventListener('load', onImageSettled, { once: true });
image.addEventListener('error', onImageSettled, { once: true });
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

+9 -64
View File
@@ -4,18 +4,11 @@ export interface PortfolioProject {
description: string; description: string;
languages: string[]; languages: string[];
icon?: string; icon?: string;
iconWidth?: number;
iconHeight?: number;
CircleIcon?: boolean; CircleIcon?: boolean;
} }
export interface PortraitHighlight { export interface PortraitHighlight {
image: string; image: string;
avifSrcset: string;
webpSrcset: string;
width: number;
height: number;
alt: string;
text: string; text: string;
} }
@@ -81,69 +74,27 @@ export const global: Global = {
], ],
portraitHighlights: [ portraitHighlights: [
{ {
image: 'assets/optimized/portrait/me-960.jpg', image: 'assets/portrait/me.png',
avifSrcset:
'assets/optimized/portrait/me-480.avif 480w, assets/optimized/portrait/me-720.avif 720w, assets/optimized/portrait/me-960.avif 900w',
webpSrcset:
'assets/optimized/portrait/me-480.webp 480w, assets/optimized/portrait/me-720.webp 720w, assets/optimized/portrait/me-960.webp 900w',
width: 900,
height: 1154,
alt: 'Portrait of Julian Lechner',
text: 'ME', text: 'ME',
}, },
{ {
image: 'assets/optimized/portrait/love-960.jpg', image: 'assets/portrait/love.jpg',
avifSrcset:
'assets/optimized/portrait/love-480.avif 480w, assets/optimized/portrait/love-720.avif 720w, assets/optimized/portrait/love-960.avif 960w',
webpSrcset:
'assets/optimized/portrait/love-480.webp 480w, assets/optimized/portrait/love-720.webp 720w, assets/optimized/portrait/love-960.webp 960w',
width: 960,
height: 1065,
alt: 'Julian with his partner',
text: 'My Love', text: 'My Love',
}, },
{ {
image: 'assets/optimized/portrait/scuba-960.jpg', image: 'assets/portrait/scuba.jpg',
avifSrcset:
'assets/optimized/portrait/scuba-480.avif 480w, assets/optimized/portrait/scuba-720.avif 720w, assets/optimized/portrait/scuba-960.avif 960w',
webpSrcset:
'assets/optimized/portrait/scuba-480.webp 480w, assets/optimized/portrait/scuba-720.webp 720w, assets/optimized/portrait/scuba-960.webp 960w',
width: 960,
height: 720,
alt: 'Julian scuba diving',
text: 'Scuba Diving', text: 'Scuba Diving',
}, },
{ {
image: 'assets/optimized/portrait/trains-960.jpg', image: 'assets/portrait/trains.jpg',
avifSrcset:
'assets/optimized/portrait/trains-480.avif 480w, assets/optimized/portrait/trains-720.avif 720w, assets/optimized/portrait/trains-960.avif 960w',
webpSrcset:
'assets/optimized/portrait/trains-480.webp 480w, assets/optimized/portrait/trains-720.webp 720w, assets/optimized/portrait/trains-960.webp 960w',
width: 960,
height: 1275,
alt: 'Julian traveling by train',
text: 'Trains', text: 'Trains',
}, },
{ {
image: 'assets/optimized/portrait/traveling-960.jpg', image: 'assets/portrait/traveling.jpg',
avifSrcset:
'assets/optimized/portrait/traveling-480.avif 480w, assets/optimized/portrait/traveling-720.avif 720w, assets/optimized/portrait/traveling-960.avif 960w',
webpSrcset:
'assets/optimized/portrait/traveling-480.webp 480w, assets/optimized/portrait/traveling-720.webp 720w, assets/optimized/portrait/traveling-960.webp 960w',
width: 960,
height: 720,
alt: 'Julian traveling',
text: 'Traveling', text: 'Traveling',
}, },
{ {
image: 'assets/optimized/portrait/culture-960.jpg', image: 'assets/portrait/culture.jpg',
avifSrcset:
'assets/optimized/portrait/culture-480.avif 480w, assets/optimized/portrait/culture-720.avif 720w, assets/optimized/portrait/culture-960.avif 960w',
webpSrcset:
'assets/optimized/portrait/culture-480.webp 480w, assets/optimized/portrait/culture-720.webp 720w, assets/optimized/portrait/culture-960.webp 960w',
width: 960,
height: 723,
alt: 'Julian visiting cultural landmarks',
text: 'Culture', text: 'Culture',
}, },
], ],
@@ -163,9 +114,7 @@ export const global: Global = {
description: description:
'A support system integrating various platforms and media to offer employees a unified overview.', 'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'], languages: ['TypeScript'],
icon: 'assets/logos/synhost-dark.webp', icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
iconWidth: 230,
iconHeight: 136,
CircleIcon: false, CircleIcon: false,
}, },
{ {
@@ -174,9 +123,7 @@ export const global: Global = {
description: description:
'A support system integrating various platforms and media to offer employees a unified overview.', 'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'], languages: ['TypeScript'],
icon: 'assets/logos/synhost-dark.webp', icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
iconWidth: 230,
iconHeight: 136,
CircleIcon: false, CircleIcon: false,
}, },
{ {
@@ -185,9 +132,7 @@ export const global: Global = {
description: description:
'A support system integrating various platforms and media to offer employees a unified overview.', 'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'], languages: ['TypeScript'],
icon: 'assets/logos/synhost-dark.webp', icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
iconWidth: 230,
iconHeight: 136,
CircleIcon: false, CircleIcon: false,
}, },
], ],
+1 -13
View File
@@ -5,19 +5,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<base href="/" /> <base href="/" />
<link rel="icon" type="image/webp" href="assets/optimized/portrait/me-480.webp" /> <link rel="icon" type="image/png" href="assets/portrait/me.png" />
<link
rel="preload"
as="image"
href="assets/optimized/portrait/me-960.jpg"
imagesrcset="
assets/optimized/portrait/me-480.avif 480w,
assets/optimized/portrait/me-720.avif 720w,
assets/optimized/portrait/me-960.avif 900w
"
imagesizes="(max-width: 640px) calc(100vw - 2rem), (max-width: 900px) min(560px, calc(100vw - 2rem)), 560px"
fetchpriority="high"
/>
<title>Lechner Julian</title> <title>Lechner Julian</title>
</head> </head>
<body> <body>
+3 -23
View File
@@ -48,12 +48,7 @@ body {
radial-gradient(circle at top left, rgba(217, 162, 140, 0.22), transparent 26%), radial-gradient(circle at top left, rgba(217, 162, 140, 0.22), transparent 26%),
radial-gradient(circle at bottom right, rgba(127, 78, 63, 0.28), transparent 28%), radial-gradient(circle at bottom right, rgba(127, 78, 63, 0.28), transparent 28%),
linear-gradient(180deg, rgba(9, 9, 12, 0.4), rgba(8, 8, 11, 0.82)), linear-gradient(180deg, rgba(9, 9, 12, 0.4), rgba(8, 8, 11, 0.82)),
image-set( url('assets/background.jpg') center / cover fixed no-repeat;
url('assets/optimized/background-1920.avif') type('image/avif'),
url('assets/optimized/background-1920.webp') type('image/webp'),
url('assets/optimized/background-1920.jpg') type('image/jpeg')
)
center / cover no-repeat;
} }
a { a {
@@ -169,8 +164,7 @@ hr {
#004dff 83.33%, #004dff 83.33%,
#750787 83.33%, #750787 83.33%,
#750787 100% #750787 100%
) ) no-repeat 0 50%;
no-repeat 0 50%;
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
@@ -315,8 +309,7 @@ hr {
#004dff 83.33%, #004dff 83.33%,
#750787 83.33%, #750787 83.33%,
#750787 100% #750787 100%
) ) 0 50%;
0 50%;
transform: translate(2px, 2px); transform: translate(2px, 2px);
} }
@@ -934,19 +927,6 @@ hr {
} }
@media (max-width: 900px) { @media (max-width: 900px) {
body {
background:
radial-gradient(circle at top left, rgba(217, 162, 140, 0.18), transparent 28%),
radial-gradient(circle at bottom right, rgba(127, 78, 63, 0.22), transparent 30%),
linear-gradient(180deg, rgba(9, 9, 12, 0.42), rgba(8, 8, 11, 0.84)),
image-set(
url('assets/optimized/background-1280.avif') type('image/avif'),
url('assets/optimized/background-1280.webp') type('image/webp'),
url('assets/optimized/background-1920.jpg') type('image/jpeg')
)
center / cover no-repeat;
}
.hero-section, .hero-section,
.story-grid, .story-grid,
.legal-grid, .legal-grid,