Split structure:
references/performance-deep-dive.mdfor extended API detail,references/vue-examples.mdfor full component code.
1. Context & Cleanup
Rule: Every gsap.to() / gsap.fromTo() / gsap.timeline() must live inside a gsap.context() — directly or via ctx.add().
let ctx
onMounted(() => {
ctx = gsap.context((self) => {
// Scroll-driven (auto-collected)
gsap.to('.item', { y: 0, scrollTrigger: { ... } })
// One-time setup (auto-collected)
gsap.set(el, { transformPerspective: 900 })
// Event-handler tweens (collected via ctx.add)
self.add('onHover', (el, x, y) => {
gsap.to(el, { rotationX: x, rotationY: y, overwrite: 'auto' })
})
}, scopeRef.value) })
onUnmounted(() => ctx?.revert()) // kills ALL tweens + ScrollTriggers + restores styles
**Composable coexistence**: When `useReveal()` (own internal context) coexists with interactive animations, create a **separate** context for the interactive part.
---
## 2. GPU Acceleration
### Transforms only — never layout properties
```js
// GOOD — GPU-composited, no layout recalc
gsap.to(el, { x: 100, y: 50, rotation: 45, scale: 1.2, opacity: 0.8 })
// BAD — triggers layout/reflow on every frame
gsap.to(el, { left: 100, top: 50, width: '120%', height: 200 })
force3D
Defaults to "auto" — uses translate3d() during animation (promotes to compositor layer), switches back to 2D when done to free GPU memory.
| Value | Behaviour |
|---|---|
"auto" |
3D during tween, 2D after — default, usually best |
true |
Stay 3D permanently — use for elements animated repeatedly (mousemove) |
false |
Always 2D — avoids sub-pixel blurring on upscaled images |
gsap.to(el, { x: 100, force3D: true }) // repeated interaction target
gsap.to(img, { scale: 1, force3D: false }) // crisp image, no blur
3. will-change Lifecycle
Set on interaction start, release when animation completes. Never permanent in CSS.
self.add('applyEffect', (el) => {
el.style.willChange = 'transform' // promote to GPU layer
gsap.to(el, { scale: 1.03, ...HOVER_IN })
})
self.add('resetEffect', (el) => {
gsap.to(el, {
scale: 1, ...HOVER_OUT,
onComplete: () => { el.style.willChange = 'auto' } // release layer
})
})
4. High-Frequency Animations
overwrite: 'auto'
Kills in-progress tweens on the same target/properties. Required on all rapid-fire tweens (mousemove, scroll callbacks).
gsap.to(el, { x: pos, overwrite: 'auto', duration: 0.3 })
gsap.quickTo() — animated updates (50-250% faster)
Pre-creates a reusable tween function. Ideal for cursor followers, parallax.
const xTo = gsap.quickTo('#cursor', 'x', { duration: 0.4, ease: 'power3' })
const yTo = gsap.quickTo('#cursor', 'y', { duration: 0.4, ease: 'power3' })
el.addEventListener('mousemove', (e) => { xTo(e.clientX); yTo(e.clientY) })
gsap.quickSetter() — immediate updates (no animation)
Skips unit conversion, relative values, parsing — raw speed for per-frame sets.
const setX = gsap.quickSetter('#el', 'x', 'px')
const setY = gsap.quickSetter('#el', 'y', 'px')
// inside ticker or mousemove
setX(mouseX); setY(mouseY)
When to use which:
| Method | Animates? | Speed | Use case |
|---|---|---|---|
gsap.to() |
Yes | Normal | General animation |
gsap.quickTo() |
Yes | 50-250% faster | Repeated cursor/scroll-driven animation |
gsap.quickSetter() |
No (instant) | Fastest | Per-frame direct value piping |
gsap.set() |
No (instant) | Normal | General one-off sets |
5. Lazy Rendering & Layout Thrashing
GSAP defers first-render writes to the end of the current tick — avoids read/write/read/write layout thrashing automatically. Default lazy: true.
- Do not set
lazy: falseunless you have a specific reason. - Zero-duration tweens (
duration: 0) havelazy: falseby default.
autoAlpha vs opacity
autoAlpha is identical to opacity but also toggles visibility: hidden when value reaches 0 — prevents pointer events and improves browser rendering of invisible elements.
gsap.set(els, { autoAlpha: 0 }) // hidden + invisible
gsap.to(els, { autoAlpha: 1, duration: 0.8 }) // fades in, sets visibility: inherit
Use autoAlpha instead of opacity for reveal animations.
6. ScrollTrigger Optimisation
batch() — staggered reveal of many elements
Creates one ScrollTrigger per target, batches callbacks within a time window.
ScrollTrigger.batch('.animate-item', {
onEnter: (els) => gsap.to(els, { autoAlpha: 1, y: 0, stagger: 0.1, overwrite: true }),
onLeave: (els) => gsap.to(els, { autoAlpha: 0, y: -20, overwrite: true }),
})
Key properties
| Property | What it does |
|---|---|
scrub: true |
Links progress 1:1 with scroll — use only for transform/opacity |
scrub: 0.5 |
Smooth catch-up (0.5s lag) — less jittery than true |
once: true |
Fires once then self-destructs — less memory |
fastScrollEnd: true |
Snaps to end state if user scrolls out fast |
invalidateOnRefresh: true |
Recalculates start values on resize |
markers: true |
Dev only — remove for production |
normalizeScroll()
Forces scroll onto JS thread — consistent mobile behaviour, prevents address-bar jank.
ScrollTrigger.normalizeScroll(true)
matchMedia — responsive animations
Different animations per breakpoint — auto-reverts when breakpoint changes.
const mm = gsap.matchMedia()
mm.add('(min-width: 1024px)', () => {
gsap.to(el, { x: 500, scrollTrigger: { trigger: el, start: 'top center' } })
})
mm.add('(max-width: 1023px)', () => {
gsap.to(el, { y: 200, scrollTrigger: { trigger: el, start: 'top 80%' } })
})
7. Timeline Reuse & registerEffect
Reuse timelines — don't recreate
const tl = gsap.timeline({ paused: true })
tl.to(el, { opacity: 1, duration: 0.3 }).to(el, { y: -20, duration: 0.5 })
const show = () => tl.play()
const hide = () => tl.reverse()
registerEffect — DRY reusable animations
gsap.registerEffect({
name: 'fadeIn',
effect: (targets, config) => gsap.to(targets, { autoAlpha: 1, y: 0, duration: config.duration, stagger: config.stagger }),
defaults: { duration: 0.6, stagger: 0.08 },
extendTimeline: true,
})
// Usage
gsap.effects.fadeIn('.cards')
tl.fadeIn('.cards', { duration: 0.4 }) // on timelines too
8. Global Configuration
gsap.defaults() — inherited by every tween
gsap.defaults({ ease: 'power2.out', duration: 0.5 })
gsap.config() — engine-level settings
gsap.config({
force3D: 'auto', // default GPU behaviour
autoSleep: 120, // frames before ticker sleeps (saves mobile battery)
nullTargetWarn: false, // silence null-target warnings in production
})
Ticker & lagSmoothing
GSAP's ticker syncs with requestAnimationFrame. lagSmoothing handles CPU spikes gracefully — if the browser lags >500ms, it acts as if only 33ms passed.
gsap.ticker.lagSmoothing(500, 33) // default — usually fine
gsap.ticker.lagSmoothing(0) // disable for frame-perfect needs (video sync)
9. Shared Tween Defaults Pattern
const HOVER_IN = { duration: 0.35, ease: 'power2.out', overwrite: 'auto' }
const HOVER_OUT = { duration: 0.7, ease: 'elastic.out(1, 0.4)' }
gsap.to(el, { x: 10, force3D: true, ...HOVER_IN })
gsap.to(el, { x: 0, force3D: true, ...HOVER_OUT, onComplete: () => { el.style.willChange = 'auto' } })
10. Anti-Patterns
// BAD: bare gsap.to inside event handler — orphaned tween
el.addEventListener('mousemove', () => gsap.to(el, { x: 10 }))
// BAD: gsap.to in rapid fire without overwrite — tween pile-up
onMouseMove = () => gsap.to(el, { x: pos, duration: 0.3 })
// BAD: layout properties — triggers reflow
gsap.to(el, { width: 200, height: 100, left: 50 })
// BAD: transformPerspective on every mousemove frame
gsap.to(el, { transformPerspective: 900, rotationX: x })
// BAD: manual scrollTriggers[] alongside gsap.context
const scrollTriggers = [] // redundant — ctx.revert() collects them
// BAD: permanent will-change in CSS
.card { will-change: transform; } // never released, wastes GPU memory
// BAD: new timeline every call
const show = () => gsap.timeline().to(el, { ... }) // recreates each time
// BAD: individual ScrollTrigger per element (use batch instead)
document.querySelectorAll('.item').forEach(item => {
gsap.to(item, { scrollTrigger: { trigger: item, ... }, ... })
})
Checklist
Apply to every GSAP-using component:
Cleanup
- All tweens inside
gsap.context()orctx.add()handlers -
ctx.revert()called inonUnmounted - No manual
scrollTriggers[]array - Separate context when composable coexistence required
GPU & Rendering
- Only transform/opacity properties animated (no left/top/width/height)
-
force3D: trueon repeatedly animated elements;"auto"elsewhere -
will-changeset on interaction start, released inonComplete -
autoAlphaused instead ofopacityfor show/hide -
transformPerspectiveset once viagsap.set(), not per-frame
High-Frequency
-
overwrite: 'auto'on all rapid-fire tweens -
quickTo()for repeated animated cursor/scroll updates -
quickSetter()for per-frame direct value piping
ScrollTrigger
-
ScrollTrigger.batch()for many similar elements -
once: trueon one-shot triggers -
invalidateOnRefresh: trueon responsive layouts -
scrubuses smoothing number (e.g.0.5) not baretruewhen possible -
markersremoved in production
Organisation
- Shared tween defaults extracted to constants
- Reusable animations registered via
gsap.registerEffect() - Timelines created once and played/reversed, not recreated
References
references/performance-deep-dive.md— Extended API details for quickTo, quickSetter, ScrollTrigger.batch, matchMedia, registerEffect, tickerreferences/vue-examples.md— Full Vue/Nuxt component implementations:- 3D tilt card with composable coexistence (separate contexts)
- Multi-concept page with all event handlers in a single context via ctx.add()