ghlee.dev
will-change의 숨겨진 의미
2024. 4. 14.will-change
라는 CSS 속성을 알고 있는가? 이를 MDN에 검색해 보면 다음과 같이 설명되어 있다.
will-change CSS 속성은 요소에 예상되는 변화의 종류에 관한 힌트를 브라우저에 제공케 한다. 그래서 실제 요소가 변화되기 전에 미리 브라우저는 적절하게 최적화할 수 있다. 이러한 종류의 최적화는 잠재적으로 성능 비용이 큰 작업을 그것이 실제로 요구되기 전에 미리 실행함으로써 페이지의 반응성을 증가시킬 수 있다.
꽤 추상적으로 느껴지는 설명이다. 아쉽게도 MDN 문서의 설명만으로는 이 속성의 작동 방식을 완전히 이해하기는 어렵다. 설명이 자세하지 않은 탓에 잘 모르고 남용하는 경우도 꽤 있는데 실제로 어느 웹사이트의 코드에서는 will-change 속성을 트랜지션이 일어나는 모든 요소에 덕지덕지 선언해놓은 것도 보았다.
will-change 속성을 이해하려면 렌더링 파이프라인에 대한 선행 지식이 필요하다. 브라우저의 렌더링 파이프라인
혹은 RenderingNG
키워드로 검색하면 다양한 자료가 있으니 참고하길 바란다.
이 글에서는 will-change 속성을 살펴보기 앞서 몇 가지 실험을 먼저 한 다음 살펴보겠다.
렌더링 프로세스의 파이프라인 최적화
크로미움 기준으로 렌더링 프로세스 안에서의 파이프라인을 잠깐 살펴보자.
흔히 렌더링 파이프라인은 초기 렌더링 시 HTML 파싱부터 시작하여 렌더 트리를 작성하는 파이프라인이 많이 알려져 있다. 해당 이미지는 초기 렌더링이 끝나고 클릭, 스크롤 같은 유저 인풋 이벤트가 있을 때 나타나는 파이프라인이다.
실제로는 항상 다섯 단계의 작업을 거칠 필요는 없다. 상황에 따라 레이아웃 단계를 생략하거나 레이아웃 단계와 페인트 단계를 둘 다 생략한다.
우리가 컨트롤할 수 있는 선에서 가장 최적화가 잘 된 경우는 레이아웃 단계와 페인트 단계를 모두 생략하는 렌더링이다.
많이 알려진 예시로 요소(element)의 위치 이동에 대해 생각해 보자. 요소의 위치를 바꾸는 애니메이션을 넣을 때 left
와 right
대신 transform
속성을 이용하여 리플로우(reflow) 혹은 리페인트(repaint)를 줄이는 최적화 이야기를 많이 접했을 거다. 참고로 리플로우와 리페인트를 줄인다는 게 레이아웃 단계와 페인트 단계를 생략한다는 말과 유사하다고 생각하면 편하다.
현재 기준으로 크로미움 관련 문서와 코드에서는 리플로우, 리페인트라는 용어는 거의 나오지 않는다. (어디 출처인 걸까..?)
transform 속성을 이용하는 방법에는 두 가지가 있는데 JavaScript를 이용한 스타일 속성 변경과 CSS 애니메이션이 있다. 사실 transform 속성을 쓰라는 말은 CSS 애니메이션 한정으로 이야기한 거긴 하다. 왜냐하면 순수 자바스크립트만으로는 완전한 최적화가 힘들기 때문이다.
Transform 테스트
클릭 이벤트로 아래 CSS 애니메이션을 실행해 보고 성능 프로파일을 확인해 보자.
CSS Animation을 사용한 transform 변경
Recalculate
에서 Pre-Paint
와 Layerize
를 거쳐서 컴포지터 스레드에 Commit
되는 과정을 볼 수 있다. 앞서 언급한 대로 레이아웃 과정과 페인트 과정이 생략된 걸 확인할 수 있다.
또한 박스 요소만 별도 레이어로 분리하여 GPU 프로세스(현재 크로미움에서는 Viz 프로세스라 불린다.)를 이용하여 화면에 그리게 된다.
"Pre-Paint는 페인트 아닌가요?"라고 생각할 수 있는데 이는 페인트 단계와는 다르다. Pre-Paint는 기존에 캐싱 되어있는 페인트 정보에 변경 사항이 있는지 확인하는 무효화(Invalidation) 확인 단계와 프로퍼티 트리(Property Tree)를 구성하는 단계로 세분화할 수 있는데 여기서는 무효화 확인을 하여 페인트 단계로 넘어가야 하는지 판단한 것으로 추측된다.
그렇다면 JavaScript로 transform
을 바꿔보면 어떨까?
JavaScript를 사용한 transform 변경
두 방법의 파이프라인은 동일하게 레이아웃과 페인팅 과정을 생략하는 걸 알 수 있다. 그렇다면 두 방법의 차이는 완전히 없는 걸까?
이번엔 크롬 개발자 도구의 레이어 탭으로 가보자.
CSS 애니메이션은 배경과 박스 요소가 분리되어 있지만 JavaScript를 사용한 박스 요소는 배경과 하나의 레이어에 함께 있다. 참고로 우측의 분리되어 있는 긴 직사각형은 스크롤 바 영역인데 항상 별도의 레이어로 분리되어 있다.
레이어가 분리되어 있으면 GPU 프로세스에서 필요한 레이어만 화면에 그리면 되니 오버헤드가 훨씬 줄어든다.
그러면 JavaScript로 transform을 사용하면 레이어는 영영 분리시킬 수 없는 걸까?
가운데 요소가 will-change 속성을 추가한 박스 요소이다.
놀랍게도 해당 요소에 will-change 속성을 추가하면 Layerize 과정에서 레이어가 분리된다. CSS 애니메이션과 마찬가지로 GPU 프로세스에서 필요한 레이어만 그릴 수 있게 된다.
Left 테스트
테스트를 통해 will-change는 레이어를 분리시켜주는 훌륭한 기능이 있다는 걸 알 수 있었지만 이제까지 우리는 transform으로만 테스트를 해보았다.
그러면 리플로우와 리페인트를 일으킨다는 악명 높은(?) left
와 right
는 어떨까? will-change를 사용하면 마찬가지로 레이어가 분리되는 걸까?
결론을 얘기하면 맞긴 하지만 단순히 레이어만 분리시키는 기능만 있는 건 아니다.
우선 left
속성만을 이용하여 요소를 이동시켜보자.
CSS 애니메이션을 이용하여 left
속성을 변경한 프로파일이다. transform 속성과 달리 레이아웃과 페인트 단계를 진행한다. 자바스크립트로 left
속성을 변경해도 유사한 결과가 나오며 또한 두 방법 모두 레이어도 분리되어 있지 않다.
그렇다면 이번엔 will-change: left
를 박스 요소에 적용시켜보자.
Transform 테스트를 했던 경험으로 이번에도 레이어가 분리되어지는건 유추할 수 있었는데 성능 프로파일에서 예상치 못한 결과가 나왔다.
성능 프로파일을 자세히 보면 Layout은 진행하지만 놀랍게도 Paint가 생략된 걸 알 수 있다. 테스트 결과만 보고 추측해 보면 left
속성을 변경했을 때는 Layout 단계에서 레이아웃 객체와 레이아웃 트리(혹은 렌더링 트리)를 갱신해야 하는데 이 과정만큼은 생략할 수 없었던 걸로 추측된다.
여담으로 프로파일에 Hit Test는 뭔지 의아해할 수 있는데 이는 브라우저가 받은 사용자 인풋을 확인하는 과정이며 성능 기록 도중에 MouseMove 이벤트 같은 게 같이 측정되어 포함된 것으로 추측된다.
어찌 됐든 이번 테스트를 통해 will-change
속성이 단순히 레이어만 분리해 주는 기능이 아니라 어떤 종류의 속성을 적용시키냐에 따라 메인 스레드의 작업을 줄인 것처럼 브라우저가 할 수 있는 최선의(?) 최적화를 해준다는 걸 알 수 있다.
마치며
이제까지 will-change
속성에 대해 몇 가지 테스트를 해보았다. 이번 테스트에서 이 속성이 구체적으로 어떤 최적화가 되어지는지 모두 알 수는 없었다.
그도 그럴게 일단 will-change를 적용할 수 있는 대상이 다양할뿐더러 사전에 정의된 CSS가 무엇이냐에 따라 최적화 경우가 또 달라질 수 있다고 생각된다. Transform 테스트에서는 레이어가 분리되는 최적화만을 보긴 했지만 사실 레이어를 분리시키는 것이 목적이라면 이를 위한 속성은 또 있다. 예를 들어 z-index
라든지 position
계열 속성들이 그 경우다. (물론 이 속성들이 최적화를 위해 존재하는 건 아니긴 하다.)
또한 렌더링 파이프라인은 commit
이 끝이 아니다. 이는 오로지 렌더러 프로세스의 메인 스레드에서 진행되는 파이프라인의 끝이며 이후 컴포지터 스레드와 GPU 프로세스에서의 과정도 남아있다. 이후 파이프라인에서의 최적화도 확인하기 위해선 chrome://tracing
도구를 이용해야 구체적으로 알 수 있지 않을까 싶은데 누군가가 시도해 보길 기대한다. (혹은 크로미움 코드를 자세히 분석하거나..)
다른 얘기지만 이번에 테스트로 쓰인 개발자 도구의 레이어 탭은 크로미움 팀에서 Deprecated 시키려고 고민 중이라는데 사실 사용량이 굉장히 적고 일부 버그도 있다고 한다. 크로미움 팀은 해당 안건에 대해서 크로미움 이슈 페이지에서 피드백을 받고 있다. 다른 사람들의 피드백을 보다 보면 레이어 탭을 어떻게 활용해왔는지 엿볼 수 있으니 재미 삼아 한번 보는 것도 괜찮아 보인다.
혹시나 테스트를 해보고 싶을 사람을 위해 깃허브에 테스트에 사용된 코드를 올려두겠다. (너무 간단한 코드라 직접 짜보셔도 된다.)