애니메이션을 translateY 대신 scale로 변경해 CLS 제거

Chrome DevTools의 레이아웃 변경 원인 분석 결과 애니메이션 × 3이 CLS 0.17의
주요 원인으로 지목됨. translateY 애니메이션을 레이아웃 변경으로 카운트하는
케이스가 있음.

- StaggerGroup: y:30 → scale:0.97 로 변경. scale/opacity는 compositor-only
  속성이라 layout/paint 없이 GPU만 사용
- BossCrystal 루트 애니메이션도 동일하게 scale 기반으로 변경
- 자식 motion.div에 will-change: transform, opacity 명시적 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-22 00:42:25 +09:00
parent 669b358460
commit f63c1e06c5
2 changed files with 12 additions and 11 deletions

View file

@ -3,19 +3,20 @@ import { motion } from 'framer-motion'
/**
* 자식을 motion.div 감싸 순차 페이드인.
* 기본값은 프로미스나인 사이트와 동일 (y 30, duration 0.4, 간격 0.1s).
* translateY 대신 scale 쓰는 이유: Chrome translateY 애니메이션을
* 레이아웃 변경(CLS)으로 카운트하는 케이스가 있어서. scale compositor-only CLS 0.
*
* @param {number} staggerDelay - 자식 간격 ()
* @param {number} yOffset - 시작 y 오프셋 (px)
* @param {number} scaleFrom - 시작 scale (기본 0.97)
* @param {number} duration - 자식 애니메이션 지속시간 ()
*/
export default function StaggerGroup({
children,
className,
style,
staggerDelay = 0.1,
yOffset = 30,
duration = 0.4,
staggerDelay = 0.08,
scaleFrom = 0.97,
duration = 0.35,
}) {
const containerVariants = {
hidden: {},
@ -23,10 +24,10 @@ export default function StaggerGroup({
}
const itemVariants = {
hidden: { opacity: 0, y: yOffset },
hidden: { opacity: 0, scale: scaleFrom },
show: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration },
},
}
@ -42,7 +43,7 @@ export default function StaggerGroup({
{Children.map(children, (child, i) => (
child == null || child === false
? null
: <motion.div variants={itemVariants} key={i}>{child}</motion.div>
: <motion.div variants={itemVariants} key={i} style={{ willChange: 'transform, opacity' }}>{child}</motion.div>
))}
</motion.div>
)

View file

@ -73,9 +73,9 @@ export default function BossCrystal() {
return (
<motion.div
className="h-full"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.03 }}
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.35 }}
style={{ willChange: 'transform, opacity' }}
>
{isLoading ? (