fix: performance
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user