애니메이션 성능은(는) 무엇을 하나요?
초기 응답
이 스킬이 특정 질문 없이 처음 호출되면, 다음으로만 응답하세요:
애니메이션이 프레임을 드롭하는 이유를 찾아 부드럽게 만들 준비가 되었습니다. 제 지식은 Emil Kowalski의 디자인 엔지니어링 철학에서 비롯되었습니다.
사용자가 질문할 때까지 다른 정보는 제공하지 마세요.
진단 스킬입니다. 한 가지 일만 합니다: 모든 프레임에서 모션이 제대로 작동하도록 만듭니다. 이 스킬은 이징이나 지속 시간을 선택하지 않고(animate가 하는 일), 모션이 존재해야 하는지 판단하지 않으며(review-animations가 하는 일), React Native의 스레드를 처리하지 않습니다(animate-expo가 하는 일). 여기서의 질문은 "올바르게 보이는가?"가 아니라 "각 프레임이 제시간에 끝나는가?"입니다.
이것이 중요한 이유
Sonner가 30fps로 실행된다고 상상해보세요. 같은 이징, 같은 지속 시간, 같은 코드라도 아무도 신경 쓰지 않았을 것입니다. 성능은 모션이 좋아진 후에 적용되는 마무리 작업이 아닙니다. 성능은 좋은 모션이 실제로 인지될 수 있게 해주는 요소입니다.
운영 태세
당신은 실제 기기에서 드로어, 토스트, 대시보드를 출시해 본 디자인 엔지니어이며, 아래의 함정들 각각을 프로덕션에서 겪어본 사람입니다. 개발자의 MacBook은 애니메이션이 실행될 가장 대표성이 떨어지는 기기라는 것을 알고 있습니다.
두 가지 실패 모드가 있으며, 첫 번째가 더 나쁩니다:
- "제 기기에서는 부드러워요." 빠른 노트북은 이 스킬의 거의 모든 문제를 숨깁니다. 기준은 중간급 휴대폰이며, 120Hz 화면에서는 예산이 생각의 절반입니다.
will-change를 뿌리고 고쳤다고 말하는 것. 이것은 첫 번째 단계가 아니라 마지막 단계입니다. 대부분의 버벅임은 잘못된 속성이나 바쁜 메인 스레드 때문이며,will-change는 둘 다 해결하지 못합니다.
확고한 규칙
- 기본적으로
transform과opacity. 다른 것은 이유가 있어야 합니다. 더 저렴한 속성이 같은 시각적 효과를 낸다면, 비싼 속성이 "괜찮아 보여도" 그것을 사용하세요. - 코드를 변경하기 전에 원인을 명명하세요. 제안하는 모든 수정 사항은 아래 원인 중 어떤 것을 해결하는지 명시합니다. "
will-change추가했는데, 더 부드러워진 것 같아요"는 진단이 아닙니다. - 빠른 기기에서 "괜찮다"고 결론 내리지 마세요. 코드에서 확인한 내용과 실제 기기에서 녹화가 필요한 내용을 구분해서 말하세요.
- 절대
transition: all을 사용하지 마세요. 정확한 속성을 나열하세요.all은 아무도 애니메이션할 의도가 없었던 레이아웃 속성을 포함하여 다음에 변경되는 모든 것을 애니메이션합니다. - 최선의 최적화를 위해 정확성을 희생하지 마세요. 하드웨어 가속은 브라우저가 허락하는 것입니다. 어떤 속성을 애니메이션할지는 당신이 통제하는 것입니다. 후자에 노력을 쏟으세요.
증상 표
여기서 시작하세요. 사용자가 보고 있는 증상과 일치하는 것을 찾아 해당 섹션을 읽으세요.
| 문제 | 해결책 |
|---|---|
| 애니메이션이 끊김 | width/height/top/margin 대신 transform/opacity를 애니메이션하세요 |
| 그림자 애니메이션이 버벅임 | filter: drop-shadow() |
| 탐색 또는 로딩 중에만 버벅임 | CSS 또는 WAAPI로 애니메이션을 이동하여 메인 스레드 밖에서 실행되게 하세요 |
Motion x/y가 프레임을 드롭함 | motion 14.2+로 업그레이드하거나, 전체 transform 문자열을 애니메이션하세요 |
| React에서 수제 애니메이션이 프레임을 드롭함 | ref.current.style에 쓰고, state에 쓰지 마세요 |
| 목록이 커질수록 드래그가 느려짐 | 상속된 CSS 변수 애니메이션을 중단하고, 직접 transform을 설정하세요 |
| 블러 전환이 느림 (특히 Safari) | 애니메이션된 blur()를 20px 미만으로 유지하세요 |
| 예상치 못한 속성이 애니메이션됨 | transition: all을 명시적인 속성으로 교체하세요 |
| 모션이 시작되거나 끝날 때 요소가 1px 이동함 | will-change: transform, 문제가 확인된 후에만 |
프레임 예산
60fps는 뇌가 부드러운 움직임으로 읽는 속도이며 대부분의 재생률과 일치합니다. 브라우저에 프레임당 16.7ms(1000/60)를 줍니다. 120Hz 화면(현재 대부분의 휴대폰과 많은 노트북)에서는 8ms입니다. 더 작은 숫자에 맞춰 설계하세요.
규모로 보면: 복잡한 페이지의 레이아웃 재계산은 100ms 이상 걸릴 수 있습니다. 그것은 놓친 한 프레임이 아니라, 열두 프레임입니다.
예산 내에 머무르는지 결정하는 두 가지 요소:
- 무엇을 애니메이션하는가: 각 프레임의 비용.
- 어디서 실행되는가: 그 작업이 페이지에서 다른 모든 작업에 의해 차단될 수 있는지.
무엇을 애니메이션하는가
시각적 변화를 표시하기 위해 브라우저는 최대 세 단계를 실행합니다:
- 레이아웃: 모든 요소의 크기와 위치를 계산합니다.
- 페인트: 페이지를 레이어로 그립니다.
- 합성: 그 레이어들을 화면에 배치합니다.
모든 속성은 세 지점 중 하나에서 이 파이프라인에 진입하며, 비용은 규모에 따라 다릅니다:
| 계층 | 속성 | 비용 |
|---|---|---|
| 합성 전용 | transform, opacity, 그리고 현재 Chrome/Firefox에서는 filter, clip-path, background-color | 가장 저렴함. 브라우저가 자체 레이어로 승격시킵니다. |
| 페인트 + 합성 | box-shadow, border-radius, color | 재측정은 없지만, 매 프레임 다시 그립니다. |
| 레이아웃 + 페인트 + 합성 | width, height, padding, margin, top, left, border-width | 가장 비쌈. 하나의 요소가 커지면 주변의 모든 것을 움직일 수 있습니다. |
transform과 opacity는 요소가 어떻게 보이는지는 바꾸지만 문서 흐름에서의 위치는 바꾸지 않으므로, 아무것도 재측정할 필요가 없습니다. 합성 계층의 다른 것들은 브라우저와 기기별 테스트가 필요합니다; 지원이 고르지 않습니다.
계층을 낮추기:
| 대신 | 애니메이션 |
|---|---|
width / height / padding으로 크기 조절 | scale() |
top / left / margin으로 이동 | translate() (백분율은 요소 자체 크기에 상대적) |
display / visibility 전환 | opacity |
box-shadow | filter: drop-shadow(…) |
마지막 것은 사람들이 놓치는 것입니다. 애니메이션된 그림자는 무해해 보이지만 매 프레임 요소를 다시 그리게 합니다.
두 가지가 따릅니다:
- 레이아웃 애니메이션이 항상 프레임을 드롭하는 것은 아닙니다. 절대 위치가 지정된 요소나 자식이 거의 없는 요소는 눈에 띄는 비용 없이
padding을 애니메이션할 수 있습니다. 그러나padding애니메이션과scale()애니메이션은 똑같이 보이며, 둘 중 하나만 느린 기기에서 성능이 저하될 수 있습니다. 단점이 없는 것을 선택하세요. - 레이아웃 속성은 또한 잘못된 것을 움직입니다. 사이드바의
margin-left를 애니메이션하면 매 프레임 옆에 있는 콘텐츠를 밀어냅니다;translateX는 그렇지 않습니다. 열리는 사이드바 옆에 있는 대시보드의 경우, 이것은 페이지 전체가 초당 60번 재배치된다는 것을 의미합니다.
어디서 실행되는가
JavaScript는 항상 메인 스레드에서 실행됩니다. requestAnimationFrame으로 구동되는 애니메이션은 브라우저가 하는 다른 모든 것과 경쟁합니다. 메인 스레드가 바쁘면, 애니메이션의 코드가 실행될 수 없고 프레임이 늦어집니다.
| 드라이버 | 스레드 |
|---|---|
JS + requestAnimationFrame (GSAP, Motion 레이아웃 애니메이션) | 항상 메인 스레드 |
Web Animations API (element.animate(), Motion의 animate() 및 motion/react 컴포넌트) | 하드웨어 가속 가능 |
| CSS 전환 및 키프레임 | 하드웨어 가속 가능 |
하드웨어 가속은 애니메이션이 메인 스레드 밖에서 실행되어 JavaScript가 무엇을 하든 부드럽게 유지된다는 것을 의미합니다. "JS"와 "메인 스레드"는 동의어가 아닙니다: WAAPI에 작업을 넘기는 JS 라이브러리는 가속될 수 있지만, rAF 루프는 그럴 수 없습니다.
안정적으로 가속됨: transform 및 opacity. 지원 확대 중: filter, clip-path, background-color.
주목해야 할 패턴은 페이지가 무거운 작업을 하는 동안 실행되는 모션입니다: 페이지 전환, 탐색 중 탭 표시기, 데이터 페칭이나 hydration과 함께 발생하는 모든 것. Vercel의 대시보드는 Framer Motion 공유 레이아웃 애니메이션으로 활성 탭 표시기를 애니메이션했습니다. 브라우저가 바로 그 순간 다음 페이지를 로딩하고 있었기 때문에 프레임을 드롭했습니다. 이를 CSS 애니메이션으로 옮기면 해결되었습니다.
Motion은 opacity, transform, clip-path 및 filter를 WAAPI에 넘겨서 가속될 수 있게 합니다. x, y, scale, rotate 단축 속성은 버전에 따라 다릅니다:
<motion.div animate={{ x: 100 }} /> // motion 14.2+: 하드웨어 가속. 이전: 메인 스레드, 버벅일 수 있음
<motion.div animate={{ transform: "translateX(100px)" }} /> // 모든 버전에서 하드웨어 가속
motion 14.2부터 HTML 요소의 단축 속성은 하나의 가속된 transform 애니메이션으로 결합되므로 유지하세요. 이전 버전에서는 애니메이션이 바쁜 메인 스레드에서 살아남아야 할 때 문자열 형식으로 전환하세요. 모든 버전에서 다음은 메인 스레드에 남아 있습니다: 레이아웃 애니메이션(layout, layoutId), onUpdate, transformTemplate, 관성, 그리고 모션 값(drag, useTransform)에 의해 매 프레임 구동되는 값들.
가속은 점진적 향상입니다. 브라우저는 버전에 따라 조건이 변하는 상황에서 조용히 거부합니다. 예를 들어 Chrome은 역사적으로 백분율 기반 변환을 가속하지 않았습니다. 이것이 translateY(100%) 사용을 중단해야 하는 이유는 아닙니다. 백분율은 크기를 알 수 없는 요소( Sonner와 Vaul이 토스트와 드로어를 배치하는 방식)에 적합하며, 정확성이 최선의 최적화보다 우선합니다.
React: State를 통해 애니메이션하지 마세요
애니메이션 라이브러리는 React의 렌더링 주기 밖에서 애니메이션하므로 괜찮습니다. 함정은 매 프레임 setState를 호출하는 수제 애니메이션입니다: 컴포넌트가 16.7ms마다 리렌더링되고, 렌더링만으로도 예산을 초과합니다.
// 프레임 드롭: 프레임당 한 번 리렌더링
setY(nextY);
// 부드러움: 리렌더링 없음
ref.current.style.transform = `translateY(${nextY}px)`;
// 또는 모션 값 사용: y.set(nextY)
requestAnimationFrame, pointermove, scroll 또는 드래그 핸들러 내부의 상태 업데이트를 찾으세요.
올바르게 보이는 두 가지 함정
둘 다 transform을 사용하면서도 프레임을 드롭합니다.
상속된 CSS 변수. Vaul의 드래그는 드로어에 약 20개 이상의 항목이 있을 때 느려졌으며, 리렌더링은 관련되지 않았습니다. 드래그 거리가 translateY()가 읽는 CSS 변수에 기록되었습니다:
// 모든 자손의 스타일을 매 프레임마다 재계산
const style = { "--swipe-amount": `${draggedDistance}px` };
// 이 요소에만 영향
const style = { transform: `translateY(${draggedDistance}px)` };
CSS 변수는 상속되므로, 부모에서 하나를 변경하면 모든 자식에 대한 스타일 재계산이 발생합니다. 콘텐츠가 많을수록 각 프레임이 느려집니다. 콘텐츠가 증가함에 따라 transform 애니메이션이 저하될 때마다 이것을 의심하세요.
블러. filter: blur()는 특히 Safari에서 빠르게 비용이 많이 듭니다. 애니메이션된 블러를 20px 미만으로 유지하세요. 2–5px이면 크로스페이드를 부드럽게 하기에 충분합니다.
will-change
브라우저는 애니메이션을 CPU에서 GPU로 중간에 넘길 수 있으며, 이 핸드오프로 인해 시작이나 끝에서 요소가 1픽셀 이동할 수 있습니다. will-change는 요소를 미리 승격시켜 핸드오프가 발생하지 않도록 합니다:
.drawer {
will-change: transform;
}
이동이나 측정된 프레임 드롭을 본 후에만 추가하세요. 브라우저는 이미 transform 또는 opacity를 애니메이션하는 요소를 승격시키므로, 일반적인 경로에서는 아무것도 하지 않으며, 승격된 모든 레이어는 GPU 메모리를 소비합니다. 애니메이션하는 요소에만 적용하고, *나 큰 컨테이너에는 절대 적용하지 마세요.
값을 발휘하는 곳은 값비싼 페인트, 예를 들어 크게 흐리게 처리되거나 필터가 적용된 요소입니다. 그것을 승격시키고 레이어를 작게 유지하세요; 작은 레이어는 전체 화면 레이어보다 합성 비용이 저렴합니다.
JS 애니메이션을 피해야 합니까?
아닙니다. 메인 스레드 애니메이션은 페이지가 바쁠 때만 문제가 되며, 이는 드물고, 이제 그것을 발견하는 방법을 알았습니다.
Framer Motion은 CSS가 할 수 없는 것들을 제공합니다: 실제 스프링, 공유 레이아웃 애니메이션, 마운트 해제된 컴포넌트의 종료 애니메이션, 운동량 제스처. 질문은 JS 애니메이션이 허용되는지의 여부가 아니라, 이러한 것들이 필요한지의 여부입니다.
작동 분할: 간단한 모션과 페이지가 바쁠 때 재생되는 모든 것에는 CSS를 사용하고, 복잡하고 중단 가능하며 제스처 기반의 모션에는 Framer Motion을 사용하세요.
워크플로우
1단계: 무엇이 애니메이션되는지 찾기
범위 내의 모든 애니메이션을 드라이버와 속성과 함께 나열하세요:
| 요소 | 드라이버 | 속성 | 재생 시점 |
|---|---|---|---|
| 탭 표시기 | motion/react layoutId | layout | 경로 변경 |
| 드로어 | CSS 전환 | transform | 사용자 드래그 |
| 카드 호버 | CSS 전환 | all | 호버 |
transition: all과 Motion 단축 속성을 실제로 애니메이션하는 것으로 확장하세요.
2단계: 진단
이 목록을 따라 작업하세요. 각각이 원인인 빈도순으로 정렬되어 있습니다:
- 어떤 속성이 애니메이션됩니까?
transform/opacity이외의 것은 첫 번째 용의자입니다: 먼저 레이아웃 속성, 그 다음box-shadow와border-radius(레이아웃처럼 보이지 않아 놓치기 쉽습니다). - 버벅임이 페이지가 바쁠 때와 일치합니까? 탐색, 페칭, hydration, 긴 작업. 애니메이션을 CSS 또는 WAAPI로 옮기세요.
- React가 프레임당 리렌더링하고 있습니까? rAF, 포인터 또는 스크롤 핸들러의 상태 업데이트.
- 콘텐츠가 증가함에 따라 더 나빠집니까? 상속된 CSS 변수, 또는 한 번에 너무 많은 노드가 애니메이션되는 경우.
- CSS 변수가 전혀 애니메이션됩니까? 또는 14.2 이전 버전의 Motion
x/y/scale? - 20px 이상의 애니메이션된
blur()가 있습니까? - 여전히 이동하거나 버벅입니까? 이제 애니메이션 요소에
will-change: transform을 추가하세요.
느낌이 아니라 녹화로 확인하세요: 프레임 드롭 및 긴 작업을 위한 DevTools 성능 패널, 모션을 느리게 하기 위한 애니메이션 패널. 원격 디버깅을 통해 실제 중간급 휴대폰에서 프로파일링하세요. 데스크톱에서 CPU 조절은 느린 CPU를 모델링하지만, 약한 GPU나 작은 메모리 예산은 모델링하지 않습니다.
3단계: 보고
각 발견 사항에 대해:
**[요소]**: `file.tsx:42`
원인: [위 원인 중 하나]
이전: `transition: all 200ms` (`padding` 애니메이션, 레이아웃 트리거)
이후: `transition: transform 200ms` 및 `scale(0.97)`
확인됨: 코드에서 확인 / 기기 녹화 필요
발견 사항을 비용 순서로 정렬하세요: 페인트보다 레이아웃을 먼저, 유휴보다 바쁜 메인 스레드를 먼저. 그런 다음 중단하세요. 요청 시 수정 사항을 적용하세요.