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 Image from "next/image";
import { motion, type Variants } from "framer-motion";
import { m, type Variants } from "framer-motion";
import Link from "next/link";
import { Utensils, Umbrella, Home, Wind, Hotel, Star, MapPin, ArrowRight } from "lucide-react";
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">
{/* Eyebrow */}
<motion.p
<m.p
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-[10px] uppercase tracking-[0.3em] text-[var(--color-moy-gold)] mb-8 font-medium"
>
{dict.hero.eyebrow}
</motion.p>
</m.p>
{/* Main heading */}
<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} />
</h1>
<motion.p
<m.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
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"
>
{dict.hero.subtitle}
</motion.p>
</m.p>
<motion.div
<m.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
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" />
</Link>
</Button>
</motion.div>
</m.div>
</div>
{/* Scroll indicator */}
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
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"
>
<span className="text-[9px] uppercase tracking-[0.25em] text-white/50">{dict.hero.scroll}</span>
<motion.div
<m.div
animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.6, repeat: Infinity, ease: "easeInOut" }}
className="w-[1px] h-8 bg-gradient-to-b from-white/50 to-transparent"
/>
</motion.div>
</m.div>
</section>
{/* ── 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="grid grid-cols-2 md:grid-cols-4 gap-12 md:gap-6">
{STATS.map((s, i) => (
<motion.div
<m.div
key={s.label}
custom={i}
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">
{s.label}
</p>
</motion.div>
</m.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">
{SERVICES.map((service, i) => (
<motion.div
<m.div
key={service.id}
custom={i}
initial="hidden"
@@ -263,7 +263,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
{service.desc}
</p>
</div>
</motion.div>
</m.div>
))}
</div>
</div>
@@ -281,7 +281,7 @@ export default function HomeClient({ lang, dict }: { lang: string; dict: any })
<div className="space-y-20">
{VENUES.map((venue, i) => (
<motion.div
<m.div
key={venue.id}
initial={{ opacity: 0, y: 40 }}
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" />
</Link>
</div>
</motion.div>
</m.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">
{TESTIMONIALS.map((t, i) => (
<motion.div
<m.div
key={i}
custom={i}
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>
</div>
</div>
</motion.div>
</m.div>
))}
</div>
</div>
+9 -9
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import { m, AnimatePresence } from "framer-motion";
import { X, ZoomIn } from "lucide-react";
import { Badge } from "@/app/components/ui/badge";
@@ -60,13 +60,13 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
{/* Grid */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 pb-28">
<motion.div
<m.div
layout
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
>
<AnimatePresence mode="popLayout">
{filtered.map((img) => (
<motion.div
<m.div
key={img.src}
layout
initial={{ opacity: 0, scale: 0.94 }}
@@ -97,16 +97,16 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
>
{img.cat}
</Badge>
</motion.div>
</m.div>
))}
</AnimatePresence>
</motion.div>
</m.div>
</div>
{/* Lightbox */}
<AnimatePresence>
{lightbox && (
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -119,7 +119,7 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
>
<X size={28} />
</button>
<motion.div
<m.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
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"
/>
<p className="text-center text-white font-serif text-xl mt-4">{lightbox.title}</p>
</motion.div>
</motion.div>
</m.div>
</m.div>
)}
</AnimatePresence>
</div>
+6 -3
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Cormorant_Garamond, Montserrat } from "next/font/google";
import Navbar from "../components/Navbar";
import Footer from "../components/Footer";
import { MotionProvider } from "../components/MotionProvider";
import "../globals.css";
import { Locale, getDictionary } from "../dictionaries";
@@ -66,9 +67,11 @@ export default async function RootLayout({
/>
</head>
<body className="min-h-full flex flex-col">
<Navbar lang={locale} dict={dict.navigation} />
<main className="flex-1">{children}</main>
<Footer lang={locale} dict={dict.footer} />
<MotionProvider>
<Navbar lang={locale} dict={dict.navigation} />
<main className="flex-1">{children}</main>
<Footer lang={locale} dict={dict.footer} />
</MotionProvider>
</body>
</html>
);
+3 -3
View File
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { m, AnimatePresence } from "framer-motion";
import { Plus, Minus } from "lucide-react";
import { SectionHeader } from "@/app/components/ui/section-header";
import { Badge } from "@/app/components/ui/badge";
@@ -26,7 +26,7 @@ function FaqItem({ q, a }: { q: string; a: string }) {
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
<m.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
@@ -34,7 +34,7 @@ function FaqItem({ q, a }: { q: string; a: string }) {
className="overflow-hidden"
>
<p className="text-gray-500 text-sm leading-relaxed pb-5">{a}</p>
</motion.div>
</m.div>
)}
</AnimatePresence>
</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 { usePathname } from "next/navigation";
import { Menu, X, Globe } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { m, AnimatePresence } from "framer-motion";
import { Locale } from "../dictionaries";
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 */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
<m.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
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>
</div>
</div>
</motion.div>
</m.div>
)}
</AnimatePresence>
</header>
+3 -3
View File
@@ -2,7 +2,7 @@
import * as React from "react"
import Image from "next/image"
import { motion } from "framer-motion";
import { m } from "framer-motion";
import { cn } from "@/lib/utils";
/* ── Base Card ── */
@@ -15,7 +15,7 @@ interface CardProps {
export function Card({ children, hover = true, glass = false, className }: CardProps) {
return (
<motion.div
<m.div
whileHover={hover ? { y: -6 } : undefined}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
className={cn(
@@ -27,7 +27,7 @@ export function Card({ children, hover = true, glass = false, className }: CardP
)}
>
{children}
</motion.div>
</m.div>
);
}
+3 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { motion, useInView } from "framer-motion";
import { m, useInView } from "framer-motion";
import { useRef } from "react";
import { cn } from "@/lib/utils";
@@ -26,7 +26,7 @@ export function TextReveal({
return (
<span ref={ref} className={cn("inline", className)}>
{words.map((word, i) => (
<motion.span
<m.span
key={i}
className="inline-block mr-[0.28em] last:mr-0"
initial={{ opacity: 0, y: 22, filter: "blur(4px)" }}
@@ -42,7 +42,7 @@ export function TextReveal({
}}
>
{word}
</motion.span>
</m.span>
))}
</span>
);
+3 -2
View File
@@ -1,4 +1,5 @@
import 'server-only';
import { cache } from 'react';
const dictionaries = {
en: () => import('../dictionaries/en.json').then((module) => module.default),
@@ -11,6 +12,6 @@ export const i18n = {
locales: ['tr', 'en'],
} as const;
export const getDictionary = async (locale: Locale) => {
export const getDictionary = cache(async (locale: Locale) => {
return dictionaries[locale]?.() ?? dictionaries.tr();
};
});