Performance: Mobile-Optimierungen für besseren PageSpeed
- Portrait-Bild von 1.2MB auf ~20KB optimiert (300px/600px + WebP) - Hero-Animation entfernt für schnelleren LCP - WebP srcset für alle Projekt-Bilder (6 Größen: 200-1200px) - Korrekte sizes-Attribute für Mobile (350px statt 1200px) - Supabase lazy laden via dynamischen Import - Admin-Check verzögert mit requestIdleCallback - Hauptbundle von 461KB auf 358KB reduziert 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
Phone,
|
||||
LayoutDashboard,
|
||||
} from "lucide-react";
|
||||
import { supabase } from "../utils/supabaseClient";
|
||||
// Supabase wird lazy geladen um Initial Bundle zu reduzieren
|
||||
import { ContactInfo } from "./ContactInfo";
|
||||
import { NavLink } from "./NavLink";
|
||||
import LanguageSwitcher from "./LanguageSwitcher";
|
||||
@@ -52,9 +52,21 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
};
|
||||
|
||||
// Prüfe Admin-Status und registriere den Scroll-Handler
|
||||
// Scroll-Handler
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 0);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
// Prüfe Admin-Status verzögert und lazy (nach Initial Render)
|
||||
useEffect(() => {
|
||||
const checkAdmin = async () => {
|
||||
// Dynamischer Import von Supabase - wird erst geladen wenn benötigt
|
||||
const { supabase } = await import("../utils/supabaseClient");
|
||||
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
if (error) {
|
||||
console.error("Error checking admin status:", error);
|
||||
@@ -74,13 +86,13 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
setIsAdmin(adminData.isAdmin);
|
||||
}
|
||||
};
|
||||
checkAdmin();
|
||||
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 0);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
// Verzögere Admin-Check bis nach dem Initial Paint
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => checkAdmin());
|
||||
} else {
|
||||
setTimeout(checkAdmin, 2000);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const navigationLinks = [
|
||||
|
||||
@@ -63,25 +63,34 @@ const Hero = () => {
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-20 relative" style={{ zIndex: 30 }}>
|
||||
{/* Profile Image */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
{/* Profile Image - keine Animation beim Initial Load für bessere LCP */}
|
||||
<div
|
||||
className="w-56 h-56 sm:w-72 sm:h-72 mb-8 rounded-full overflow-hidden border-2 border-zinc-800"
|
||||
style={{ transform: 'translateZ(0)', backfaceVisibility: 'hidden' }}
|
||||
style={{ transform: 'translateZ(0)' }}
|
||||
>
|
||||
<img
|
||||
src="/portrait.jpg"
|
||||
alt={t('pages.home.hero.image.alt')}
|
||||
className="w-full h-full object-cover"
|
||||
loading="eager"
|
||||
fetchPriority="high"
|
||||
width={288}
|
||||
height={288}
|
||||
style={{ transform: 'scale(1.01)' }}
|
||||
/>
|
||||
</motion.div>
|
||||
<picture>
|
||||
<source
|
||||
srcSet="/portrait-300.webp 300w, /portrait-600.webp 600w"
|
||||
sizes="(max-width: 640px) 224px, 288px"
|
||||
type="image/webp"
|
||||
/>
|
||||
<source
|
||||
srcSet="/portrait-300.jpg 300w, /portrait-600.jpg 600w"
|
||||
sizes="(max-width: 640px) 224px, 288px"
|
||||
type="image/jpeg"
|
||||
/>
|
||||
<img
|
||||
src="/portrait-600.jpg"
|
||||
alt={t('pages.home.hero.image.alt')}
|
||||
className="w-full h-full object-cover"
|
||||
loading="eager"
|
||||
fetchPriority="high"
|
||||
width={288}
|
||||
height={288}
|
||||
decoding="sync"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
{/* Title and Name */}
|
||||
<motion.p
|
||||
|
||||
@@ -29,9 +29,12 @@ const PortfolioCard: React.FC<PortfolioCardProps> = ({ project }) => {
|
||||
visible: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
const imagePath = `/images/projects/${project.slug}/cover.jpg`;
|
||||
const imagePathWebP = `/images/projects/${project.slug}/cover.webp`;
|
||||
const srcSet = `/images/projects/${project.slug}/cover-400w.jpg 400w, /images/projects/${project.slug}/cover-800w.jpg 800w, /images/projects/${project.slug}/cover-1200w.jpg 1200w`;
|
||||
const basePath = `/images/projects/${project.slug}`;
|
||||
const imagePath = `${basePath}/cover.jpg`;
|
||||
const srcSetJpg = `${basePath}/cover-200w.jpg 200w, ${basePath}/cover-300w.jpg 300w, ${basePath}/cover-400w.jpg 400w, ${basePath}/cover-600w.jpg 600w, ${basePath}/cover-800w.jpg 800w, ${basePath}/cover-1200w.jpg 1200w`;
|
||||
const srcSetWebP = `${basePath}/cover-200w.webp 200w, ${basePath}/cover-300w.webp 300w, ${basePath}/cover-400w.webp 400w, ${basePath}/cover-600w.webp 600w, ${basePath}/cover-800w.webp 800w, ${basePath}/cover-1200w.webp 1200w`;
|
||||
// Mobile: ~350px, Tablet: ~400px, Desktop 2-col: ~550px, Desktop 3-col: ~400px
|
||||
const imageSizes = "(max-width: 640px) 350px, (max-width: 768px) 400px, (max-width: 1024px) 550px, 400px";
|
||||
|
||||
// Verwende technologies anstelle von tags wenn verfügbar
|
||||
const displayTags = project.technologies || project.tags;
|
||||
@@ -63,17 +66,24 @@ const PortfolioCard: React.FC<PortfolioCardProps> = ({ project }) => {
|
||||
{/* Image Container */}
|
||||
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900">
|
||||
<picture>
|
||||
<source srcSet={imagePathWebP} type="image/webp" />
|
||||
<source
|
||||
srcSet={srcSetWebP}
|
||||
sizes={imageSizes}
|
||||
type="image/webp"
|
||||
/>
|
||||
<source
|
||||
srcSet={srcSetJpg}
|
||||
sizes={imageSizes}
|
||||
type="image/jpeg"
|
||||
/>
|
||||
<img
|
||||
src={imagePath}
|
||||
srcSet={srcSet}
|
||||
sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
|
||||
alt={project.title}
|
||||
className="absolute inset-0 w-full h-full object-cover transition-transform duration-700
|
||||
group-hover:scale-110"
|
||||
loading="lazy"
|
||||
width={1200}
|
||||
height={675}
|
||||
width={400}
|
||||
height={225}
|
||||
decoding="async"
|
||||
style={{ transformOrigin: 'center center' }}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user