애니메이션을 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:
parent
669b358460
commit
f63c1e06c5
2 changed files with 12 additions and 11 deletions
|
|
@ -3,19 +3,20 @@ import { motion } from 'framer-motion'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 자식을 각 motion.div 로 감싸 순차 페이드인.
|
* 자식을 각 motion.div 로 감싸 순차 페이드인.
|
||||||
* 기본값은 프로미스나인 사이트와 동일 (y 30, duration 0.4, 간격 0.1s).
|
* translateY 대신 scale 을 쓰는 이유: Chrome 이 translateY 애니메이션을
|
||||||
|
* 레이아웃 변경(CLS)으로 카운트하는 케이스가 있어서. scale 은 compositor-only 라 CLS 0.
|
||||||
*
|
*
|
||||||
* @param {number} staggerDelay - 자식 간 간격 (초)
|
* @param {number} staggerDelay - 자식 간 간격 (초)
|
||||||
* @param {number} yOffset - 시작 y 오프셋 (px)
|
* @param {number} scaleFrom - 시작 scale (기본 0.97)
|
||||||
* @param {number} duration - 각 자식 애니메이션 지속시간 (초)
|
* @param {number} duration - 각 자식 애니메이션 지속시간 (초)
|
||||||
*/
|
*/
|
||||||
export default function StaggerGroup({
|
export default function StaggerGroup({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
staggerDelay = 0.1,
|
staggerDelay = 0.08,
|
||||||
yOffset = 30,
|
scaleFrom = 0.97,
|
||||||
duration = 0.4,
|
duration = 0.35,
|
||||||
}) {
|
}) {
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: {},
|
hidden: {},
|
||||||
|
|
@ -23,10 +24,10 @@ export default function StaggerGroup({
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: yOffset },
|
hidden: { opacity: 0, scale: scaleFrom },
|
||||||
show: {
|
show: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
scale: 1,
|
||||||
transition: { duration },
|
transition: { duration },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +43,7 @@ export default function StaggerGroup({
|
||||||
{Children.map(children, (child, i) => (
|
{Children.map(children, (child, i) => (
|
||||||
child == null || child === false
|
child == null || child === false
|
||||||
? null
|
? 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>
|
</motion.div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,9 @@ export default function BossCrystal() {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="h-full"
|
className="h-full"
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, scale: 0.97 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 0.03 }}
|
transition={{ duration: 0.35 }}
|
||||||
style={{ willChange: 'transform, opacity' }}
|
style={{ willChange: 'transform, opacity' }}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue