ما زالت GSAP أقوى مكتبة للحركة على الويب، ومنذ الإصدار 3.13 أصبحت كل إضافاتها مجانية: ScrollTrigger وSplitText وMorphSVG وغيرها. لكن React يرسم ويعيد الرسم ويزيل المكونات، ويشغّل التأثيرات مرتين في وضع التطوير، ومكتبة حركة تعدّل DOM مباشرةً تحتاج إلى بعض العناية لتنسجم معه.
هذا هو الإعداد الذي أستخدمه في مشاريع Next.js App Router، ومنها هذا الموقع.
التثبيت
npm install gsap @gsap/reactتوفّر @gsap/react الخطّاف useGSAP، الذي يتولى الجزء الذي يخطئ فيه الناس عادةً: التنظيف.
النمط الأساسي
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP)
export function Hero() {
const container = useRef<HTMLDivElement>(null)
useGSAP(
() => {
gsap.from('.hero-line', { yPercent: 100, opacity: 0, stagger: 0.08, duration: 0.9, ease: 'power3.out' })
},
{ scope: container },
)
return (
<div ref={container}>
<h1 className="hero-line">Design</h1>
<h1 className="hero-line">and build</h1>
</div>
)
}تحدث هنا ثلاثة أمور:
'use client'. تتعامل GSAP مع DOM، لذا يجب أن يعمل المكوّن في المتصفح. اجعل الجزء المتحرك صغيرًا، واترك بقية الصفحة مكونات خادم (Server Components).scope: container. المحددات مثل.hero-lineلا تطابق إلا داخل هذا المكوّن، فلا تحرّك نسختان عناصر بعضهما أبدًا.- التنظيف التلقائي. كل حركة (tween) وخط زمني وScrollTrigger يُنشأ داخل الدالة يُلغى عند إزالة المكوّن. ولهذا أيضًا لا يخلّف التشغيل المزدوج في وضع التطوير حركات مكررة.
إعادة التشغيل عند التغيير
مرّر dependencies لإعادة التشغيل عند تغيّر البيانات، وrevertOnUpdate إن كان يجب التراجع عن التشغيل السابق أولًا:
useGSAP(
() => {
gsap.to('.bar', { scaleX: progress, transformOrigin: 'left', duration: 0.4 })
},
{ scope: container, dependencies: [progress] },
)معالجات الأحداث: contextSafe
الحركات التي تُنشأ بعد تشغيل الخطّاف، في معالج نقرة مثلًا، لا تُسجَّل للتنظيف تلقائيًا. غلّف تلك الدوال بـ contextSafe:
const { contextSafe } = useGSAP({ scope: container })
const onEnter = contextSafe((event: React.MouseEvent<HTMLElement>) => {
gsap.to(event.currentTarget, { scale: 1.04, duration: 0.3 })
})
return <button onMouseEnter={onEnter}>Hover me</button>ScrollTrigger
سجّلها مرة واحدة (يكفي استدعاء على مستوى الوحدة)، وأنشئ المشغّلات داخل useGSAP:
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP, ScrollTrigger)
export function RevealSection({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLElement>(null)
useGSAP(
() => {
gsap.utils.toArray<HTMLElement>('.reveal').forEach((el) => {
gsap.from(el, {
y: 40,
opacity: 0,
duration: 0.8,
ease: 'power2.out',
scrollTrigger: { trigger: el, start: 'top 85%' },
})
})
},
{ scope: ref },
)
return <section ref={ref}>{children}</section>
}التثبيت والربط بالتمرير
useGSAP(
() => {
gsap.timeline({
scrollTrigger: {
trigger: '.story',
start: 'top top',
end: '+=200%',
scrub: 1, // follows the scrollbar, with 1 s of smoothing
pin: true, // holds the section while the timeline plays
},
})
.to('.story-image', { scale: 1.2 })
.to('.story-text', { yPercent: -50 }, '<')
},
{ scope: ref },
)التحديث بعد تغيّر التخطيط
تقيس ScrollTrigger المواضع عند إنشائها. فإذا غيّرت الصور أو الخطوط أو المحتوى المتأخر ارتفاع الصفحة بعد ذلك، تنحرف مواضع البداية والنهاية. استدعِ ScrollTrigger.refresh() بعد تحميلها:
document.fonts.ready.then(() => ScrollTrigger.refresh())وإعطاء الصور أبعادًا صريحة (أو aspect-ratio) يمنع معظم هذا من الأساس.
مشكلات خاصة بـ Next.js
تغيير الصفحات وموضع التمرير
في App Router، يؤدي التنقل بين الصفحات إلى إزالة مكونات الصفحة القديمة (فينظّف useGSAP مشغّلاتها) وتركيب مكونات الصفحة الجديدة. إن رأيت مشغّلات تنطلق في مواضع خاطئة بعد التنقل، فالسبب عادةً أن الصفحة الجديدة قاست المواضع قبل تحميل صورها؛ حدّثها بعد التحميل كما في الأعلى.
مكونات الخادم لا تتحرك
لا يمكنك وضع useGSAP في Server Component. وبدلًا من تحويل الصفحة كلها إلى Client Component، غلّف الجزء المتحرك فقط بغلاف صغير يعمل في المتصفح، ومرّر إليه المحتوى المعروض على الخادم بوصفه children. يبقى المحتوى في HTML (وهذا جيد لتحسين محركات البحث وLCP)، وتُضاف الحركة فوقه.
تجنّب وميض المحتوى قبل تحريكه
يعمل gsap.from() بعد الإماهة (hydration)، فيظهر العنصر لإطار واحد في حالته النهائية ثم يقفز إلى حالة البداية. هناك حلّان:
- استخدم
gsap.fromTo()مع CSS يضبط حالة البداية (opacity: 0في ملف الأنماط)، واقبل أن ظهور العنصر يعتمد على JavaScript؛ وهذا للعناصر غير الأساسية فقط. - والأفضل لكل ما يظهر في أعلى الصفحة: الحركة من حالة مرئية (إزاحة صغيرة في
yدون تغيير الشفافية)، ليكون الرسم الأول صحيحًا من البداية. وسيشكرك LCP على ذلك؛ راجع مؤشرات أداء الويب للمواقع المتحركة.
إمكانية الوصول: matchMedia
تتيح لك gsap.matchMedia() إنشاء الحركات فقط عندما يتحقق استعلام وسائط، وتلغيها تلقائيًا عندما يتوقف تحققه:
useGSAP(
() => {
const mm = gsap.matchMedia()
mm.add('(prefers-reduced-motion: no-preference)', () => {
gsap.from('.card', { y: 60, opacity: 0, stagger: 0.1, scrollTrigger: '.cards' })
})
},
{ scope: container },
)يحصل الزوار الذين يفضّلون تقليل الحركة على المحتوى دون الحركة. والواجهة نفسها تتعامل مع التأثيرات الخاصة بالحاسوب فقط ('(min-width: 768px)').
نصائح للأداء
- حرّك
xوyوscaleوrotationوopacity؛ إذ تحوّلها GSAP إلى transforms. - استخدم
gsap.quickTo()للقيم التي تتحدث باستمرار، مثل عنصر يتبع المؤشر؛ فهي تعيد استخدام حركة واحدة بدل إنشاء حركة جديدة مع كل حدث. - أبقِ عدد مشغّلات ScrollTrigger المتزامنة معقولًا، واجمع العناصر المتشابهة بـ
ScrollTrigger.batch().
الخلاصة
useGSAPمعscopeلكل مكوّن متحركcontextSafeللحركات المنشأة في معالجات الأحداث- سجّل الإضافات مرة واحدة، وأنشئ مشغّلات ScrollTrigger داخل
useGSAP - حدّث ScrollTrigger بعد تحميل الخطوط والصور
- أبقِ المحتوى في مكونات الخادم وحرّكه عبر أغلفة تعمل في المتصفح
- احترم
prefers-reduced-motionباستخدامgsap.matchMedia()

