fix: performance

This commit is contained in:
2026-04-27 15:53:19 +02:00
parent ef10c2efe7
commit 65e5249512
5 changed files with 142 additions and 35 deletions
+43 -24
View File
@@ -53,22 +53,54 @@ function getProfile(relativePath) {
} }
if (normalizedRelativePath.startsWith('logos/')) { if (normalizedRelativePath.startsWith('logos/')) {
return { maxWidth: 640, quality: 90, alphaQuality: 92, nearLossless: true }; return { maxWidth: 216, quality: 78, alphaQuality: 88, variants: [64, 128, 216] };
} }
if (normalizedRelativePath.startsWith('portrait/')) { if (normalizedRelativePath.startsWith('portrait/')) {
return { maxWidth: 1600, quality: 84, alphaQuality: 92 }; return { maxWidth: 900, quality: 82, alphaQuality: 90, variants: [128, 320, 480, 640, 900] };
} }
return { maxWidth: 1600, quality: 82, alphaQuality: 92 }; return { maxWidth: 1600, quality: 82, alphaQuality: 92 };
} }
function getTargetRelativePath(relativePath, width) {
const parsedPath = path.parse(relativePath);
const fileName =
width === undefined ? `${parsedPath.name}.webp` : `${parsedPath.name}-${width}.webp`;
return path.join(parsedPath.dir, fileName);
}
async function createWebpBuffer(inputBuffer, profile, width) {
const image = sharp(inputBuffer, { animated: false }).rotate();
const metadata = await image.metadata();
const targetWidth = width ?? profile.maxWidth;
let pipeline = image;
if ((metadata.width ?? 0) > targetWidth) {
pipeline = pipeline.resize({
width: targetWidth,
fit: 'inside',
withoutEnlargement: true,
});
}
return pipeline
.webp({
quality: profile.quality,
alphaQuality: profile.alphaQuality,
effort: 6,
smartSubsample: true,
nearLossless: profile.nearLossless ?? false,
})
.toBuffer();
}
async function writeOptimizedAsset(sourceFilePath) { async function writeOptimizedAsset(sourceFilePath) {
const relativePath = path.relative(sourceRoot, sourceFilePath); const relativePath = path.relative(sourceRoot, sourceFilePath);
const extension = path.extname(sourceFilePath).toLowerCase(); const extension = path.extname(sourceFilePath).toLowerCase();
const targetRelativePath = passthroughExtensions.has(extension) const targetRelativePath = passthroughExtensions.has(extension)
? relativePath ? relativePath
: relativePath.replace(/\.[^.]+$/, '.webp'); : getTargetRelativePath(relativePath);
const targetFilePath = path.join(targetRoot, targetRelativePath); const targetFilePath = path.join(targetRoot, targetRelativePath);
fs.mkdirSync(path.dirname(targetFilePath), { recursive: true }); fs.mkdirSync(path.dirname(targetFilePath), { recursive: true });
@@ -87,30 +119,17 @@ async function writeOptimizedAsset(sourceFilePath) {
const profile = getProfile(relativePath); const profile = getProfile(relativePath);
const inputBuffer = fs.readFileSync(sourceFilePath); const inputBuffer = fs.readFileSync(sourceFilePath);
const inputBytes = inputBuffer.byteLength; const inputBytes = inputBuffer.byteLength;
const image = sharp(inputBuffer, { animated: false }).rotate(); const outputBuffer = await createWebpBuffer(inputBuffer, profile);
const metadata = await image.metadata();
let pipeline = image;
if ((metadata.width ?? 0) > profile.maxWidth) {
pipeline = pipeline.resize({
width: profile.maxWidth,
fit: 'inside',
withoutEnlargement: true,
});
}
const outputBuffer = await pipeline
.webp({
quality: profile.quality,
alphaQuality: profile.alphaQuality,
effort: 6,
smartSubsample: true,
nearLossless: profile.nearLossless ?? false,
})
.toBuffer();
fs.writeFileSync(targetFilePath, outputBuffer); fs.writeFileSync(targetFilePath, outputBuffer);
for (const variantWidth of profile.variants ?? []) {
const variantRelativePath = getTargetRelativePath(relativePath, variantWidth);
const variantFilePath = path.join(targetRoot, variantRelativePath);
const variantBuffer = await createWebpBuffer(inputBuffer, profile, variantWidth);
fs.writeFileSync(variantFilePath, variantBuffer);
}
return { return {
sourceRelativePath: relativePath, sourceRelativePath: relativePath,
targetRelativePath, targetRelativePath,
+4
View File
@@ -67,6 +67,8 @@
<img <img
class="hero-feature-image" class="hero-feature-image"
[src]="currentPortraitHighlight.image" [src]="currentPortraitHighlight.image"
[attr.srcset]="currentPortraitHighlight.imageSrcset"
[attr.sizes]="currentPortraitHighlight.imageSizes"
width="560" width="560"
height="420" height="420"
[alt]="currentPortraitHighlight.text + ' Image'" [alt]="currentPortraitHighlight.text + ' Image'"
@@ -143,6 +145,8 @@
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.srcset]="project.iconSrcset ?? null"
[attr.sizes]="project.iconSizes ?? null"
[alt]="project.title + ' icon'" [alt]="project.title + ' icon'"
/> />
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+43 -3
View File
@@ -4,11 +4,15 @@ export interface PortfolioProject {
description: string; description: string;
languages: string[]; languages: string[];
icon?: string; icon?: string;
iconSrcset?: string;
iconSizes?: string;
CircleIcon?: boolean; CircleIcon?: boolean;
} }
export interface PortraitHighlight { export interface PortraitHighlight {
image: string; image: string;
imageSrcset: string;
imageSizes: string;
text: string; text: string;
} }
@@ -75,26 +79,50 @@ export const global: Global = {
portraitHighlights: [ portraitHighlights: [
{ {
image: 'assets/optimized/portrait/me.webp', image: 'assets/optimized/portrait/me.webp',
imageSrcset:
'assets/optimized/portrait/me-320.webp 320w, assets/optimized/portrait/me-480.webp 480w, assets/optimized/portrait/me-640.webp 640w, assets/optimized/portrait/me.webp 900w',
imageSizes:
'(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'ME', text: 'ME',
}, },
{ {
image: 'assets/optimized/portrait/love.webp', image: 'assets/optimized/portrait/love.webp',
imageSrcset:
'assets/optimized/portrait/love-320.webp 320w, assets/optimized/portrait/love-480.webp 480w, assets/optimized/portrait/love-640.webp 640w, assets/optimized/portrait/love.webp 900w',
imageSizes:
'(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'My Love', text: 'My Love',
}, },
{ {
image: 'assets/optimized/portrait/scuba.webp', image: 'assets/optimized/portrait/scuba.webp',
imageSrcset:
'assets/optimized/portrait/scuba-320.webp 320w, assets/optimized/portrait/scuba-480.webp 480w, assets/optimized/portrait/scuba-640.webp 640w, assets/optimized/portrait/scuba.webp 900w',
imageSizes:
'(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Scuba Diving', text: 'Scuba Diving',
}, },
{ {
image: 'assets/optimized/portrait/trains.webp', image: 'assets/optimized/portrait/trains.webp',
imageSrcset:
'assets/optimized/portrait/trains-320.webp 320w, assets/optimized/portrait/trains-480.webp 480w, assets/optimized/portrait/trains-640.webp 640w, assets/optimized/portrait/trains.webp 900w',
imageSizes:
'(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Trains', text: 'Trains',
}, },
{ {
image: 'assets/optimized/portrait/traveling.webp', image: 'assets/optimized/portrait/traveling.webp',
imageSrcset:
'assets/optimized/portrait/traveling-320.webp 320w, assets/optimized/portrait/traveling-480.webp 480w, assets/optimized/portrait/traveling-640.webp 640w, assets/optimized/portrait/traveling.webp 900w',
imageSizes:
'(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Traveling', text: 'Traveling',
}, },
{ {
image: 'assets/optimized/portrait/culture.webp', image: 'assets/optimized/portrait/culture.webp',
imageSrcset:
'assets/optimized/portrait/culture-320.webp 320w, assets/optimized/portrait/culture-480.webp 480w, assets/optimized/portrait/culture-640.webp 640w, assets/optimized/portrait/culture.webp 900w',
imageSizes:
'(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Culture', text: 'Culture',
}, },
], ],
@@ -106,6 +134,9 @@ export const global: Global = {
'I currently work primarily for the viennese company SobIT GmbH. This company develops software for the healthcare industry. - Development of modern desktop, mobile, and web applications to support healthcare delivery.', 'I currently work primarily for the viennese company SobIT GmbH. This company develops software for the healthcare industry. - Development of modern desktop, mobile, and web applications to support healthcare delivery.',
languages: ['TypeScript', 'C#', 'WPF', 'YML', 'XAML'], languages: ['TypeScript', 'C#', 'WPF', 'YML', 'XAML'],
icon: 'assets/optimized/logos/sobit.webp', icon: 'assets/optimized/logos/sobit.webp',
iconSrcset:
'assets/optimized/logos/sobit-64.webp 64w, assets/optimized/logos/sobit-128.webp 128w, assets/optimized/logos/sobit.webp 216w',
iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false, CircleIcon: false,
}, },
{ {
@@ -114,7 +145,10 @@ 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: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png', icon: 'assets/optimized/logos/synhost.webp',
iconSrcset:
'assets/optimized/logos/synhost-64.webp 64w, assets/optimized/logos/synhost-128.webp 128w, assets/optimized/logos/synhost.webp 216w',
iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false, CircleIcon: false,
}, },
{ {
@@ -123,7 +157,10 @@ 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: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png', icon: 'assets/optimized/logos/synhost.webp',
iconSrcset:
'assets/optimized/logos/synhost-64.webp 64w, assets/optimized/logos/synhost-128.webp 128w, assets/optimized/logos/synhost.webp 216w',
iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false, CircleIcon: false,
}, },
{ {
@@ -132,7 +169,10 @@ 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: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png', icon: 'assets/optimized/logos/synhost.webp',
iconSrcset:
'assets/optimized/logos/synhost-64.webp 64w, assets/optimized/logos/synhost-128.webp 128w, assets/optimized/logos/synhost.webp 216w',
iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false, CircleIcon: false,
}, },
], ],
+52 -8
View File
@@ -3,22 +3,66 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta
name="description"
content="Portfolio of Julian Lechner, an Austrian full-stack developer building stable web, desktop, and business software."
/>
<base href="/" /> <base href="/" />
<link rel="icon" type="image/webp" href="assets/optimized/portrait/me.webp" /> <link rel="icon" type="image/webp" sizes="128x128" href="assets/optimized/portrait/me-128.webp" />
<title>Lechner Julian</title> <title>Lechner Julian</title>
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>
<script src="https://storage.ko-fi.com/cdn/scripts/overlay-widget.js"></script>
<script> <script>
kofiWidgetOverlay.draw('fraujulian', { (() => {
type: 'floating-chat', const widgetTitle = 'Ko-fi donation chat';
'floating-chat.donateButton.text': 'Tip me', let hasLoadedWidget = false;
'floating-chat.donateButton.background-color': '#ff5f5f',
'floating-chat.donateButton.text-color': '#fff', const titleKofiFrames = () => {
}); document.querySelectorAll('iframe[id^="kofi-wo-container"]').forEach((frame) => {
frame.setAttribute('title', widgetTitle);
});
};
const drawWidget = () => {
window.kofiWidgetOverlay.draw('fraujulian', {
type: 'floating-chat',
'floating-chat.donateButton.text': 'Tip me',
'floating-chat.donateButton.background-color': '#ff5f5f',
'floating-chat.donateButton.text-color': '#fff',
});
window.setTimeout(titleKofiFrames, 300);
};
const loadWidget = () => {
if (hasLoadedWidget) {
return;
}
hasLoadedWidget = true;
const script = document.createElement('script');
script.src = 'https://storage.ko-fi.com/cdn/scripts/overlay-widget.js';
script.async = true;
script.onload = drawWidget;
document.body.appendChild(script);
};
window.addEventListener(
'load',
() => {
const observer = new MutationObserver(titleKofiFrames);
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener('pointerdown', loadWidget, { once: true, passive: true });
window.addEventListener('touchstart', loadWidget, { once: true, passive: true });
window.addEventListener('scroll', loadWidget, { once: true, passive: true });
window.addEventListener('keydown', loadWidget, { once: true });
},
{ once: true },
);
})();
</script> </script>
</body> </body>
</html> </html>