返回部落格

在 React 和 Next.js 中使用 GSAP:useGSAP、清理與 ScrollTrigger

在 React 和 Next.js 中使用 GSAP:useGSAP、清理與 ScrollTrigger

2026年10月5日 — 動畫

發布於

2026年10月5日

閱讀時間

3 分鐘

分類

動畫

標籤

  • GSAP
  • React
  • Next.js
  • ScrollTrigger
  • Animation

(摘要)如何在 React 和 Next.js App Router 中使用 GSAP,避免記憶體洩漏和重複動畫:useGSAP、作用域、contextSafe、ScrollTrigger 與路由切換。

GSAP 依然是 Web 上功能最強的動畫庫,而且從 3.13 版本開始,所有外掛——ScrollTrigger、SplitText、MorphSVG,全部——都免費了。但 React 會渲染、重新渲染、卸載元件,並且(在開發模式下)會把副作用執行兩次。一個直接操作 DOM 的動畫庫要融入進來,需要一點技巧。

下面是我在 Next.js App Router 專案中使用的方案,本站也是如此。

安裝

bash
npm install gsap @gsap/react

@gsap/react 提供 useGSAP hook,它處理了大家最容易出錯的部分:清理。

基本用法

components/hero.tsx
'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>
  )
}

這裡發生了三件事:

  1. 'use client'。 GSAP 要操作 DOM,所以元件必須在瀏覽器中執行。讓動畫部分儘量小,頁面其餘部分保持為服務端元件。
  2. scope: container。 像 .hero-line 這樣的選擇器只會匹配當前元件內部的元素,兩個例項永遠不會互相影響。
  3. 自動清理。 回呼中建立的所有補間、時間軸和 ScrollTrigger,都會在元件卸載時還原。這也是為什麼 React 開發模式下的雙重呼叫不會留下重複的動畫。

資料變化時重新執行

傳入 dependencies 讓它在資料變化時重新執行;如果需要先撤銷上一次的效果,再加上 revertOnUpdate:

tsx
useGSAP(
  () => {
    gsap.to('.bar', { scaleX: progress, transformOrigin: 'left', duration: 0.4 })
  },
  { scope: container, dependencies: [progress] },
)

事件處理函式:contextSafe

在 hook 執行之後建立的動畫——比如在點擊事件裡——不會被自動記錄下來以便清理。用 contextSafe 包裹這些函式:

tsx
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 裡建立觸發器:

components/reveal-section.tsx
'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>
}

固定與跟隨捲動

tsx
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():

ts
document.fonts.ready.then(() => ScrollTrigger.refresh())

給圖片設定明確的尺寸(或 aspect-ratio)能從源頭避免大部分此類問題。

Next.js 特有的坑

路由切換和捲動位置

在 App Router 中,頁面間導航會卸載舊頁面的元件(於是 useGSAP 清理了它們的觸發器),並掛載新頁面的元件。如果導航後發現觸發器在錯誤的位置觸發,通常是因為新頁面在圖片載入完成前就做了測量——像上面那樣在載入後重新整理即可。

服務端元件不能做動畫

服務端元件裡不能使用 useGSAP。不要把整個頁面變成客戶端元件,而是隻用一個小的客戶端包裝元件包住動畫部分,把服務端渲染的內容作為 children 傳進去。內容保留在 HTML 中(對 SEO 和 LCP 都好),動效疊加在上面。

避免未動畫內容的閃爍

gsap.from() 在水合之後才執行,所以會有一幀元素顯示為最終狀態,然後跳到起始狀態。兩種解決辦法:

  • 使用 gsap.fromTo(),並用 CSS 設定起始狀態(樣式表中寫 opacity: 0),同時接受這個元素要依賴 JavaScript 才能顯示——只適用於非關鍵元素。
  • 對首屏內容更好的做法:從可見狀態開始動畫(小幅 y 位移,不改變透明度),這樣首次繪製就是正確的。你的 LCP 會感謝你——見動畫網站的 Core Web Vitals 優化。

無障礙:matchMedia

gsap.matchMedia() 讓你只在媒體查詢匹配時建立動畫,並在不再匹配時自動還原:

tsx
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 },
)

偏好減少動態效果的訪客會直接看到內容,沒有運動效果。同樣的 API 也能處理只在桌面端生效的效果('(min-width: 768px)')。

效能建議

  • 對 x、y、scale、rotation 和 opacity 做動畫,GSAP 會把它們對映為 transform。
  • 對持續更新的值(比如跟隨游標的元素)使用 gsap.quickTo()——它複用同一個補間,而不是每次事件都新建一個。
  • 控制同時存在的 ScrollTrigger 數量,相似元素用 ScrollTrigger.batch() 批次處理。

總結

  • 每個動畫元件都用帶 scope 的 useGSAP
  • 事件處理中建立的動畫用 contextSafe
  • 外掛註冊一次,ScrollTrigger 在 useGSAP 裡建立
  • 字型和圖片載入完成後重新整理 ScrollTrigger
  • 內容放在服務端元件中,用客戶端包裝元件新增動畫
  • 用 gsap.matchMedia() 尊重 prefers-reduced-motion

電子報

用 Email 訂閱新文章

有新文章時寄一封簡短的信給你。不發垃圾信,隨時可以取消訂閱。

小團隊的設計系統:從 Design Token 開始,而不是元件
小團隊的設計系統:從 Design Token 開始,而不是元件
下一篇