perf: optimize framer-motion bundle size and memoize dictionary fetching

- Wrap getDictionary with React cache to prevent redundant JSON parsing on the server
- Create MotionProvider to globally initialize framer-motion LazyMotion with domAnimation
- Replace standard 'motion' imports with 'm' across all animated client components (HomeClient, Navbar, GaleriClient, SSSClient, card, text-reveal) to drastically reduce First Load JS size
This commit is contained in:
mstfyldz
2026-06-04 12:50:23 +03:00
parent e1d8bae5a4
commit acf01c81fa
9 changed files with 59 additions and 44 deletions
+18 -18
View File
@@ -2,7 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { motion, type Variants } from "framer-motion"; import { m, type Variants } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
import { Utensils, Umbrella, Home, Wind, Hotel, Star, MapPin, ArrowRight } from "lucide-react"; import { Utensils, Umbrella, Home, Wind, Hotel, Star, MapPin, ArrowRight } from "lucide-react";
import { Button } from "@/app/components/ui/button"; import { Button } from "@/app/components/ui/button";
@@ -128,30 +128,30 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<div className="relative z-10 text-center px-4 max-w-5xl mx-auto"> <div className="relative z-10 text-center px-4 max-w-5xl mx-auto">
{/* Eyebrow */} {/* Eyebrow */}
<motion.p <m.p
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }} transition={{ duration: 0.6, delay: 0.1 }}
className="text-[10px] uppercase tracking-[0.3em] text-[var(--color-moy-gold)] mb-8 font-medium" className="text-[10px] uppercase tracking-[0.3em] text-[var(--color-moy-gold)] mb-8 font-medium"
> >
{dict.hero.eyebrow} {dict.hero.eyebrow}
</motion.p> </m.p>
{/* Main heading */} {/* Main heading */}
<h1 className="text-5xl md:text-7xl lg:text-8xl font-serif text-white mb-8 leading-[0.95] tracking-tight"> <h1 className="text-5xl md:text-7xl lg:text-8xl font-serif text-white mb-8 leading-[0.95] tracking-tight">
<TextReveal text={dict.hero.title} delay={0.2} /> <TextReveal text={dict.hero.title} delay={0.2} />
</h1> </h1>
<motion.p <m.p
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.9 }} transition={{ duration: 0.8, delay: 0.9 }}
className="text-base md:text-lg text-gray-300/90 mb-12 font-light max-w-xl mx-auto leading-relaxed" className="text-base md:text-lg text-gray-300/90 mb-12 font-light max-w-xl mx-auto leading-relaxed"
> >
{dict.hero.subtitle} {dict.hero.subtitle}
</motion.p> </m.p>
<motion.div <m.div
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 1.1 }} transition={{ duration: 0.7, delay: 1.1 }}
@@ -168,23 +168,23 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" /> <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
</Link> </Link>
</Button> </Button>
</motion.div> </m.div>
</div> </div>
{/* Scroll indicator */} {/* Scroll indicator */}
<motion.div <m.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: 1.6, duration: 0.6 }} transition={{ delay: 1.6, duration: 0.6 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2" className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2"
> >
<span className="text-[9px] uppercase tracking-[0.25em] text-white/50">{dict.hero.scroll}</span> <span className="text-[9px] uppercase tracking-[0.25em] text-white/50">{dict.hero.scroll}</span>
<motion.div <m.div
animate={{ y: [0, 8, 0] }} animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.6, repeat: Infinity, ease: "easeInOut" }} transition={{ duration: 1.6, repeat: Infinity, ease: "easeInOut" }}
className="w-[1px] h-8 bg-gradient-to-b from-white/50 to-transparent" className="w-[1px] h-8 bg-gradient-to-b from-white/50 to-transparent"
/> />
</motion.div> </m.div>
</section> </section>
{/* ── Marquee Strip ── */} {/* ── Marquee Strip ── */}
@@ -203,7 +203,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<div className="max-w-5xl mx-auto px-6"> <div className="max-w-5xl mx-auto px-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-12 md:gap-6"> <div className="grid grid-cols-2 md:grid-cols-4 gap-12 md:gap-6">
{STATS.map((s, i) => ( {STATS.map((s, i) => (
<motion.div <m.div
key={s.label} key={s.label}
custom={i} custom={i}
initial="hidden" initial="hidden"
@@ -218,7 +218,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<p className="text-[10px] uppercase tracking-[0.2em] text-gray-500 mt-3"> <p className="text-[10px] uppercase tracking-[0.2em] text-gray-500 mt-3">
{s.label} {s.label}
</p> </p>
</motion.div> </m.div>
))} ))}
</div> </div>
</div> </div>
@@ -236,7 +236,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-5">
{SERVICES.map((service, i) => ( {SERVICES.map((service, i) => (
<motion.div <m.div
key={service.id} key={service.id}
custom={i} custom={i}
initial="hidden" initial="hidden"
@@ -263,7 +263,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
{service.desc} {service.desc}
</p> </p>
</div> </div>
</motion.div> </m.div>
))} ))}
</div> </div>
</div> </div>
@@ -281,7 +281,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<div className="space-y-20"> <div className="space-y-20">
{VENUES.map((venue, i) => ( {VENUES.map((venue, i) => (
<motion.div <m.div
key={venue.id} key={venue.id}
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
@@ -321,7 +321,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<ArrowRight size={12} className="group-hover:translate-x-1 transition-transform" /> <ArrowRight size={12} className="group-hover:translate-x-1 transition-transform" />
</Link> </Link>
</div> </div>
</motion.div> </m.div>
))} ))}
</div> </div>
</div> </div>
@@ -341,7 +341,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{TESTIMONIALS.map((t, i) => ( {TESTIMONIALS.map((t, i) => (
<motion.div <m.div
key={i} key={i}
custom={i} custom={i}
initial="hidden" initial="hidden"
@@ -371,7 +371,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<p className="text-[10px] uppercase tracking-[0.15em] text-gray-500 mt-0.5">{t.origin}</p> <p className="text-[10px] uppercase tracking-[0.15em] text-gray-500 mt-0.5">{t.origin}</p>
</div> </div>
</div> </div>
</motion.div> </m.div>
))} ))}
</div> </div>
</div> </div>
+9 -9
View File
@@ -2,7 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion"; import { m, AnimatePresence } from "framer-motion";
import { X, ZoomIn } from "lucide-react"; import { X, ZoomIn } from "lucide-react";
import { Badge } from "@/app/components/ui/badge"; import { Badge } from "@/app/components/ui/badge";
@@ -60,13 +60,13 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
{/* Grid */} {/* Grid */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 pb-28"> <div className="max-w-7xl mx-auto px-4 sm:px-6 pb-28">
<motion.div <m.div
layout layout
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
> >
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
{filtered.map((img) => ( {filtered.map((img) => (
<motion.div <m.div
key={img.src} key={img.src}
layout layout
initial={{ opacity: 0, scale: 0.94 }} initial={{ opacity: 0, scale: 0.94 }}
@@ -97,16 +97,16 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
> >
{img.cat} {img.cat}
</Badge> </Badge>
</motion.div> </m.div>
))} ))}
</AnimatePresence> </AnimatePresence>
</motion.div> </m.div>
</div> </div>
{/* Lightbox */} {/* Lightbox */}
<AnimatePresence> <AnimatePresence>
{lightbox && ( {lightbox && (
<motion.div <m.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
@@ -119,7 +119,7 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
> >
<X size={28} /> <X size={28} />
</button> </button>
<motion.div <m.div
initial={{ scale: 0.9, opacity: 0 }} initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }} exit={{ scale: 0.9, opacity: 0 }}
@@ -135,8 +135,8 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
className="w-full h-auto max-h-[80vh] object-contain" className="w-full h-auto max-h-[80vh] object-contain"
/> />
<p className="text-center text-white font-serif text-xl mt-4">{lightbox.title}</p> <p className="text-center text-white font-serif text-xl mt-4">{lightbox.title}</p>
</motion.div> </m.div>
</motion.div> </m.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
+3
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Cormorant_Garamond, Montserrat } from "next/font/google"; import { Cormorant_Garamond, Montserrat } from "next/font/google";
import Navbar from "../components/Navbar"; import Navbar from "../components/Navbar";
import Footer from "../components/Footer"; import Footer from "../components/Footer";
import { MotionProvider } from "../components/MotionProvider";
import "../globals.css"; import "../globals.css";
import { Locale, getDictionary } from "../dictionaries"; import { Locale, getDictionary } from "../dictionaries";
@@ -66,9 +67,11 @@ export default async function RootLayout({
/> />
</head> </head>
<body className="min-h-full flex flex-col"> <body className="min-h-full flex flex-col">
<MotionProvider>
<Navbar lang={locale} dict={dict.navigation} /> <Navbar lang={locale} dict={dict.navigation} />
<main className="flex-1">{children}</main> <main className="flex-1">{children}</main>
<Footer lang={locale} dict={dict.footer} /> <Footer lang={locale} dict={dict.footer} />
</MotionProvider>
</body> </body>
</html> </html>
); );
+3 -3
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { m, AnimatePresence } from "framer-motion";
import { Plus, Minus } from "lucide-react"; import { Plus, Minus } from "lucide-react";
import { SectionHeader } from "@/app/components/ui/section-header"; import { SectionHeader } from "@/app/components/ui/section-header";
import { Badge } from "@/app/components/ui/badge"; import { Badge } from "@/app/components/ui/badge";
@@ -26,7 +26,7 @@ function FaqItem({ q, a }: { q: string; a: string }) {
</button> </button>
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{open && ( {open && (
<motion.div <m.div
initial={{ height: 0, opacity: 0 }} initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }} animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }} exit={{ height: 0, opacity: 0 }}
@@ -34,7 +34,7 @@ function FaqItem({ q, a }: { q: string; a: string }) {
className="overflow-hidden" className="overflow-hidden"
> >
<p className="text-gray-500 text-sm leading-relaxed pb-5">{a}</p> <p className="text-gray-500 text-sm leading-relaxed pb-5">{a}</p>
</motion.div> </m.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { LazyMotion, domAnimation } from "framer-motion";
export function MotionProvider({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
);
}
+3 -3
View File
@@ -5,7 +5,7 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Menu, X, Globe } from "lucide-react"; import { Menu, X, Globe } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion"; import { m, AnimatePresence } from "framer-motion";
import { Locale } from "../dictionaries"; import { Locale } from "../dictionaries";
export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) { export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
@@ -91,7 +91,7 @@ export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
{/* Mobile Nav */} {/* Mobile Nav */}
<AnimatePresence> <AnimatePresence>
{mobileMenuOpen && ( {mobileMenuOpen && (
<motion.div <m.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
@@ -114,7 +114,7 @@ export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
<Link href={switchLanguage('en')} onClick={() => setMobileMenuOpen(false)} className={`text-sm ${lang === 'en' ? 'text-[var(--color-moy-gold)] font-bold' : ''}`}>English</Link> <Link href={switchLanguage('en')} onClick={() => setMobileMenuOpen(false)} className={`text-sm ${lang === 'en' ? 'text-[var(--color-moy-gold)] font-bold' : ''}`}>English</Link>
</div> </div>
</div> </div>
</motion.div> </m.div>
)} )}
</AnimatePresence> </AnimatePresence>
</header> </header>
+3 -3
View File
@@ -2,7 +2,7 @@
import * as React from "react" import * as React from "react"
import Image from "next/image" import Image from "next/image"
import { motion } from "framer-motion"; import { m } from "framer-motion";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
/* ── Base Card ── */ /* ── Base Card ── */
@@ -15,7 +15,7 @@ interface CardProps {
export function Card({ children, hover = true, glass = false, className }: CardProps) { export function Card({ children, hover = true, glass = false, className }: CardProps) {
return ( return (
<motion.div <m.div
whileHover={hover ? { y: -6 } : undefined} whileHover={hover ? { y: -6 } : undefined}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }} transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
className={cn( className={cn(
@@ -27,7 +27,7 @@ export function Card({ children, hover = true, glass = false, className }: CardP
)} )}
> >
{children} {children}
</motion.div> </m.div>
); );
} }
+3 -3
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion, useInView } from "framer-motion"; import { m, useInView } from "framer-motion";
import { useRef } from "react"; import { useRef } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -26,7 +26,7 @@ export function TextReveal({
return ( return (
<span ref={ref} className={cn("inline", className)}> <span ref={ref} className={cn("inline", className)}>
{words.map((word, i) => ( {words.map((word, i) => (
<motion.span <m.span
key={i} key={i}
className="inline-block mr-[0.28em] last:mr-0" className="inline-block mr-[0.28em] last:mr-0"
initial={{ opacity: 0, y: 22, filter: "blur(4px)" }} initial={{ opacity: 0, y: 22, filter: "blur(4px)" }}
@@ -42,7 +42,7 @@ export function TextReveal({
}} }}
> >
{word} {word}
</motion.span> </m.span>
))} ))}
</span> </span>
); );
+3 -2
View File
@@ -1,4 +1,5 @@
import 'server-only'; import 'server-only';
import { cache } from 'react';
const dictionaries = { const dictionaries = {
en: () => import('../dictionaries/en.json').then((module) => module.default), en: () => import('../dictionaries/en.json').then((module) => module.default),
@@ -11,6 +12,6 @@ export const i18n = {
locales: ['tr', 'en'], locales: ['tr', 'en'],
} as const; } as const;
export const getDictionary = async (locale: Locale) => { export const getDictionary = cache(async (locale: Locale) => {
return dictionaries[locale]?.() ?? dictionaries.tr(); return dictionaries[locale]?.() ?? dictionaries.tr();
}; });