fix(perf): replace framer-motion with pure css animations in HeroSection to eliminate JS-induced 4.5s LCP delay

This commit is contained in:
2026-06-26 19:22:17 +02:00
parent 586b6cdee8
commit d56ce045c3
2 changed files with 49 additions and 42 deletions

View File

@@ -87,3 +87,35 @@
} }
} }
/* trigger rebuild */ /* trigger rebuild */
@layer utilities {
@keyframes hero-fade-in-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-hero-fade-in-up {
animation: hero-fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
}
@layer utilities {
@keyframes hero-zoom-out {
from {
transform: scale(1.05);
}
to {
transform: scale(1);
}
}
.animate-hero-zoom-out {
animation: hero-zoom-out 1.5s ease-out forwards;
}
}

View File

@@ -3,7 +3,6 @@
import React from 'react'; import React from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { m } from 'framer-motion';
import { Badge, Container, Heading } from '@/components/ui'; import { Badge, Container, Heading } from '@/components/ui';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
@@ -17,25 +16,7 @@ export interface HeroSectionProps {
ctaHref?: string; ctaHref?: string;
} }
const containerVariants = { // CSS animations replace Framer Motion for better LCP
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }
},
};
export const HeroSection: React.FC<HeroSectionProps> = (props) => { export const HeroSection: React.FC<HeroSectionProps> = (props) => {
const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props; const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props;
@@ -47,12 +28,9 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`} className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
> >
{bgSrc && ( {bgSrc && (
<m.div <div
key={`hero-bg-${pathname}`} key={`hero-bg-${pathname}`}
className="absolute inset-0 z-0" className="absolute inset-0 z-0 animate-hero-zoom-out"
initial={{ scale: 1.05, opacity: 1 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
> >
<Image <Image
src={bgSrc} src={bgSrc}
@@ -74,37 +52,34 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
{/* Top Fade for Header Navigation Readability */} {/* Top Fade for Header Navigation Readability */}
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" /> <div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
</m.div> </div>
)} )}
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}> <Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
<m.div <div
key={`hero-content-${pathname}`} key={`hero-content-${pathname}`}
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`} className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
variants={containerVariants}
initial="hidden"
animate="visible"
> >
{badge && ( {badge && (
<m.div variants={itemVariants}> <div className="animate-hero-fade-in-up" style={{ animationDelay: '0.1s', opacity: 0, animationFillMode: 'forwards' }}>
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg"> <Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
{badge} {badge}
</Badge> </Badge>
</m.div> </div>
)} )}
<m.div variants={itemVariants}> <div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.25s' : '0.1s', opacity: 0, animationFillMode: 'forwards' }}>
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8"> <Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title} {title}
</Heading> </Heading>
</m.div> </div>
{subtitle && ( {subtitle && (
<m.div variants={itemVariants}> <div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.4s' : '0.25s', opacity: 0, animationFillMode: 'forwards' }}>
<p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}> <p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}>
{subtitle} {subtitle}
</p> </p>
</m.div> </div>
)} )}
{ctaLabel && ctaHref && ( {ctaLabel && ctaHref && (
<m.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`}> <div className={`mt-8 animate-hero-fade-in-up ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`} style={{ animationDelay: badge ? '0.55s' : '0.4s', opacity: 0, animationFillMode: 'forwards' }}>
<Button <Button
href={ctaHref} href={ctaHref}
variant="accent" variant="accent"
@@ -115,9 +90,9 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
&rarr; &rarr;
</span> </span>
</Button> </Button>
</m.div> </div>
)} )}
</m.div> </div>
</Container> </Container>
</section> </section>
); );