feat(performance): Core Web Vitals optimizations
- Font optimization: Replace @font-face with next/font/google (DM Sans, Inter) for faster font loading and no render-blocking - Image optimization: Add Unsplash to remotePatterns, configure AVIF/WebP formats, add device/image sizes - Convert native <img> tags to next/image with proper sizing and priority for LCP images - Add optimizePackageImports for lucide-react and framer-motion to reduce bundle size - Fix CLS: Urgency message uses fixed min-height instead of animated height - Fix CLS: ProductCard quick-add button uses opacity instead of translate for hover - Convert HeroVideo scroll indicator to CSS animation - Script loading: Rybbit uses lazyOnload strategy for better INP
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
@@ -22,13 +23,16 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen w-full overflow-hidden">
|
||||
{/* Background Image with Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop')`,
|
||||
}}
|
||||
>
|
||||
{/* Background Image with Overlay - Using next/image for LCP optimization */}
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop"
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="100vw"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-black/70" />
|
||||
</div>
|
||||
|
||||
@@ -132,21 +136,16 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<motion.button
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.5, duration: 0.8 }}
|
||||
<button
|
||||
onClick={scrollToContent}
|
||||
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/60 hover:text-white transition-colors cursor-pointer"
|
||||
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/60 hover:text-white transition-colors cursor-pointer opacity-0 animate-fade-in"
|
||||
style={{ animationDelay: '1.5s', animationFillMode: 'forwards' }}
|
||||
aria-label="Scroll to content"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
|
||||
>
|
||||
<div className="scroll-indicator">
|
||||
<ChevronDown className="w-6 h-6" strokeWidth={1.5} />
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
</div>
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ export default function ProductCard({ product, index = 0, locale = "sr" }: Produ
|
||||
<Link href={`/${locale}/products/${localized.slug}`} className="group block">
|
||||
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||
{image ? (
|
||||
<img
|
||||
<Image
|
||||
src={image}
|
||||
alt={localized.name}
|
||||
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
loading="lazy"
|
||||
fill
|
||||
className="object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
||||
@@ -52,7 +53,7 @@ export default function ProductCard({ product, index = 0, locale = "sr" }: Produ
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<div className="absolute inset-x-0 bottom-0 p-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<button
|
||||
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -245,10 +245,12 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
|
||||
: "border-transparent hover:border-[#999999]"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={image.url}
|
||||
alt={image.alt || localized.name}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="100px"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
@@ -256,10 +258,13 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
|
||||
)}
|
||||
|
||||
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden flex-1">
|
||||
<img
|
||||
<Image
|
||||
src={images[selectedImage].url}
|
||||
alt={images[selectedImage].alt || localized.name}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
|
||||
{images.length > 1 && (
|
||||
@@ -307,17 +312,15 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="lg:pl-8"
|
||||
>
|
||||
<motion.div
|
||||
key={urgencyIndex}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 rounded-lg mb-4 text-sm font-medium text-left"
|
||||
>
|
||||
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
|
||||
{urgencyMessages[urgencyIndex].text}
|
||||
</motion.div>
|
||||
<div className="min-h-[52px] flex items-center">
|
||||
<div
|
||||
className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 px-4 rounded-lg mb-4 text-sm font-medium text-left w-full"
|
||||
key={urgencyIndex}
|
||||
>
|
||||
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
|
||||
{urgencyMessages[urgencyIndex].text}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
|
||||
{localized.name}
|
||||
|
||||
Reference in New Issue
Block a user