블러 애니메이션

흐리게 처리는 사용자의 주의를 돌리는 좋은 방법입니다. 일부 시각적 요소는 흐리게 표시하고 다른 요소에 포커스를 유지하면 자연스럽게 사용자의 초점이 맞춰집니다. 사용자는 흐리게 처리된 콘텐츠를 무시하고 대신 읽을 수 있는 콘텐츠에 집중합니다. 한 가지 예로 마우스를 가져가면 개별 항목에 관한 세부정보를 표시하는 아이콘 목록이 있습니다. 이 시간 동안 나머지 선택사항은 흐리게 처리되어 사용자를 새로 표시된 정보로 리디렉션할 수 있습니다.

요약

블러 애니메이션은 매우 느리기 때문에 실제로 옵션이 아닙니다. 대신 점점 더 흐리게 처리된 일련의 버전을 미리 계산하고 버전 간에 크로스 페이드합니다. 제 동료인 이구가 라이브러리를 만들어 여러분을 대신해 주었습니다. 데모를 살펴보세요.

그러나 전환 기간 없이 이 기법을 적용하면 상당히 부자연스러울 수 있습니다. 흐리게 처리되지 않은 이미지에서 블러 처리한 것으로 전환하는 블러 애니메이션은 합리적인 선택인 것 같습니다. 하지만 웹에서 이 작업을 시도해 본 적이 있다면 애니메이션이 매끄럽지 않다는 것을 알 수 있습니다. 이 데모에서 볼 수 있는 강력한 머신이 없는 경우를 예로 들 수 있습니다. 개선이 필요하신가요?

문제

마크업은 CPU에 의해 텍스처로 변환됩니다. 텍스처는 GPU에 업로드됩니다. GPU는 셰이더를 사용하여 이러한 텍스처를 프레임 버퍼에 그립니다. 흐리게 처리는 셰이더에서 발생합니다.

현재로서는 블러 애니메이션이 효율적으로 작동하도록 할 수 없습니다. 그러나 충분히 보이기는 하지만 기술적으로는 애니메이션 블러가 아닌 해결 방법을 찾을 수 있습니다. 먼저 애니메이션 블러가 느린 이유를 먼저 알아보겠습니다. 웹에서 요소를 흐리게 처리하는 방법에는 CSS filter 속성과 SVG 필터의 두 가지 방법이 있습니다. 향상된 지원과 사용 편의성 덕분에 일반적으로 CSS 필터가 사용됩니다. 안타깝게도 Internet Explorer를 지원해야 하는 경우 IE 10 및 11에서는 SVG 필터를 사용할 수 있지만 CSS 필터는 지원하지 않습니다. 좋은 소식은 흐리게 처리 애니메이션의 해결 방법이 두 기법 모두에서 작동한다는 것입니다. DevTools를 확인하여 병목 현상을 찾아보겠습니다

DevTools에서 'Paint Flashing'을 사용 설정하면 플래시가 전혀 표시되지 않습니다. 다시 페인트가 발생하지 않는 것으로 보입니다. 이는 기술적으로 정확합니다. '다시 페인트'는 CPU가 승격된 요소의 텍스처를 다시 페인트해야 한다는 의미입니다. 요소의 승격 블러가 모두 발생할 때마다 셰이더를 사용하여 GPU가 블러를 적용합니다.

SVG 필터와 CSS 필터 모두 컨볼루션 필터를 사용하여 블러를 적용합니다. 컨볼루션 필터는 모든 출력 픽셀에 대해 여러 개의 입력 픽셀을 고려해야 하므로 상당히 비용이 많이 듭니다. 이미지가 크거나 블러 반경이 클수록 효과 비용이 더 많이 듭니다.

이때 문제가 있습니다. 프레임마다 다소 비싼 GPU 작업을 실행하여 프레임 예산이 16ms로 늘어났고 결국 60fps 아래로 떨어졌습니다.

토끼굴로 내려가기

그렇다면 이 과정을 원활하게 진행하려면 어떻게 해야 할까요? 손놀림을 할 수 있어요! 실제 블러 값 (블러의 반경)에 애니메이션을 적용하는 대신 블러 값이 기하급수적으로 증가하는 몇 개의 블러된 사본을 미리 계산한 다음 opacity를 사용하여 크로스 페이드합니다.

크로스 페이드는 겹치는 불투명도 페이드 인 및 페이드 아웃의 연속입니다. 예를 들어 블러 단계가 4개인 경우 첫 번째 단계에서 페이드 아웃하는 동시에 두 번째 단계에서 페이드 인합니다. 두 번째 단계가 100% 에 도달하고 첫 번째 단계가 0%에 도달하면 두 번째 단계가 페이드 아웃되면서 세 번째 단계가 페이드 인됩니다. 이 작업이 끝나면 마지막으로 세 번째 스테이지를 페이드아웃하고 네 번째이자 최종 버전에서 페이드인합니다. 이 시나리오에서 각 단계에는 원하는 총 기간의 1⁄4이 걸립니다. 시각적으로 이는 실제 애니메이션 블러와 매우 유사합니다.

실험 결과, 흐리게 처리 반경을 단계별로 기하급수적으로 늘리면서 최상의 시각적 결과를 얻었습니다. 예: 흐리게 처리 단계가 4개 있으면 각 단계(0단계: 1px, 1단계: 2px, 2단계: 4px, 3: 8px)에 filter: blur(2^n)를 적용합니다. will-change: transform를 사용하여 블러 처리된 각 사본을 자체 레이어('승격'이라고 함)에 강제로 적용한다면 이러한 요소의 불투명도 변경 속도가 매우 빨라집니다. 이론적으로는 이를 통해 비용이 많이 드는 블러 작업을 미리 로드할 수 있습니다. 결국 논리에 결함이 있습니다. 이 데모를 실행하면 프레임 속도가 여전히 60fps 미만이며 블러가 이전보다 더 나빠지는 것을 확인할 수 있습니다.

GPU에서 사용 시간이 긴 트레이스를 표시하는 DevTools

DevTools를 빠르게 살펴보면 GPU가 여전히 매우 사용 중이며 각 프레임이 최대 90ms로 늘어나는 것을 알 수 있습니다. 이유가 무엇인가요? 블러 값은 더 이상 변경하지 않고 불투명도만 변경합니다. 어떻게 된 것일까요? 문제는 흐리게 처리 효과의 특성에 있습니다. 앞에서 설명한 것처럼 요소가 승격되고 블러 처리되면 GPU가 효과를 적용합니다. 따라서 더 이상 블러 값을 애니메이션 처리하지 않더라도 텍스처 자체는 여전히 블러가 적용되지 않으며 GPU가 모든 프레임에 다시 블러를 적용해야 합니다. 프레임 속도가 이전보다 훨씬 더 나빠지는 이유는 기본 구현에 비해 GPU가 실제로 이전보다 더 많은 작업을 하기 때문입니다. 대부분의 경우 독립적으로 블러 처리해야 하는 두 텍스처가 표시되기 때문입니다.

우리가 생각해낸 결과는 멋지지는 않지만 애니메이션을 매우 빠르게 만듭니다. 흐리게 처리할 요소를 승격하지 않는 대신 상위 래퍼를 승격합니다. 요소가 블러 처리되고 승격되면 GPU가 효과를 적용합니다. 이 때문에 데모가 느려졌습니다. 요소가 블러 처리되었지만 승격되지 않으면 대신 가장 가까운 상위 텍스처로 블러가 래스터화됩니다. 여기서는 승격된 상위 래퍼 요소입니다. 블러 처리된 이미지는 이제 상위 요소의 텍스처이며 향후 모든 프레임에 재사용할 수 있습니다. 이는 흐리게 처리된 요소가 애니메이션이 아님을 알고 있으므로 이를 캐시하는 것이 실제로 유익하기 때문에 작동합니다. 다음은 이 기법을 구현하는 데모입니다. Moto G4는 이러한 접근 방식에 대해 어떻게 생각하는지 궁금해요. 스포일러 주의: 멋지다고 생각합니다.

GPU의 유휴 시간이 많은 부분을 추적하는 DevTools

이제 GPU에 충분한 헤드룸과 매끄러운 60fps를 확보했습니다. 우리가 해냈어!

프로덕션화

이 데모에서는 DOM 구조를 여러 번 복제하여 다양한 강도로 흐리게 처리할 콘텐츠 사본을 만들었습니다. 작성자의 CSS 스타일이나 JavaScript에 의도하지 않은 부작용이 발생할 수 있으므로 프로덕션 환경에서 이 속성이 어떻게 작동하는지 궁금할 수 있습니다. 맞아요. Shadow DOM을 시작합니다.

대부분의 사람들은 Shadow DOM맞춤 요소에 '내부' 요소를 연결하는 방법으로 생각하지만, 이는 격리 및 성능 프리미티브이기도 합니다. JavaScript 및 CSS는 Shadow DOM 경계를 관통할 수 없으므로 개발자의 스타일이나 애플리케이션 로직을 방해하지 않고 콘텐츠를 복제할 수 있습니다. 래스터화할 각 사본의 <div> 요소가 이미 있으며 이제 이러한 <div>를 섀도 호스트로 사용합니다. attachShadow({mode: 'closed'})를 사용하여 ShadowRoot를 만들고 콘텐츠 사본을 <div> 자체 대신 ShadowRoot에 첨부합니다. 사본의 스타일이 원본과 동일하게 지정되도록 하려면 모든 스타일시트도 ShadowRoot에 복사해야 합니다.

일부 브라우저는 Shadow DOM v1을 지원하지 않으며 이러한 브라우저에서는 콘텐츠를 복제하는 것으로 대체됩니다. 문제가 발생하지 않는 것이 최선입니다. ShadyCSS와 함께 Shadow DOM 폴리필을 사용할 수 있지만 라이브러리에서는 이를 구현하지 않았습니다.

자, 이제 Chrome의 렌더링 파이프라인을 진행한 후 여러 브라우저에서 효율적으로 블러 애니메이션을 적용할 수 있는 방법을 알아냈습니다.

결론

이러한 종류의 효과를 가볍게 사용해서는 안 됩니다. DOM 요소를 복사하여 자체 레이어에 강제로 적용하므로 저사양 기기의 한계를 뛰어넘을 수 있습니다. 모든 스타일시트를 각 ShadowRoot에 복사하는 작업도 잠재적인 성능 위험이 있으므로 LightDOM의 사본에 영향을 받지 않도록 로직과 스타일을 조정할지 아니면 ShadowDOM 기법을 사용할지 결정해야 합니다. 그러나 때로는 우리의 기법이 가치 있는 투자가 될 수 있습니다. GitHub 저장소의 코드와 데모를 살펴보고 궁금한 점이 있으면 Twitter를 통해 문의해 주세요.