최근에 emoji-picker-element
에서 흥미로운 성능 이슈가 발견되었습니다.
19만 개의 맞춤 이모지가 있는 페디 인스턴스를 사용 중인데 ... 이모지 선택기를 열면 페이지가 최소 1초 동안 멈추고 그 후에도 한동안 전반적인 성능이 악화됩니다.
마스토돈이나 페디버스(Fediverse)가 낯서실 수 있는데요. 이 서비스들은 서버마다 Slack, Discord 등과 같이 자체 사용자 지정 이모지가 있을 수 있습니다. 19,000개(이 경우 실제로는 20,000개에 가깝습니다)는 매우 드문 경우이지만, 전례가 없는 것은 아닙니다.
자, 그래서 버그 제보자의 문제 상황을 재현해봤는데...맙소사, 정말 느렸어요.
여기에는 여러 가지 문제가 있었습니다.
<button>
과 <img>
를 필요로 하기 때문에 4만 개의 요소가 있음을 의미했습니다.다행히도 저는 <img loading="lazy">
를 사용하고 있었기 때문에 2만 개의 이미지가 한 번에 모두 다운로드되지는 않았습니다. 하지만 어쨌든 4만 개의 요소를 렌더링하는 것은 고통스러울 정도로 느릴 것입니다(Lighthouse는 1,400개 이하를 권장합니다!).
물론 첫 번째 든 생각은 "도대체 누가 2만 개의 사용자 지정 이모지를 가지고 있지?"라는 것이었습니다. 두 번째 생각은 "하.. 가상화를 해야겠군." 이었습니다.
저는 1) 복잡하고 2) 필요하지 않다고 생각했고 3) 접근성에 영향을 미칠 수 있다는 이유로 emoji-picker-element
의 가상화를 피하고 있었기 때문입니다.
전에도 겪어본 적 있는 일입니다. 피나포어(Pinafore)는 기본적으로 하나의 큰 가상 목록입니다. 저는 ARIA feed role을 사용하고, 모든 계산을 직접 수행했으며, "무한 스크롤"을 싫어하는 분들을 위해 이를 비활성화하는 옵션을 추가했습니다. 이런 상황이 처음은 아니었어요! 저는 작성해야 할 코드가 너무 많아서 얼굴을 찡그리고 있었고, "자그마한" ~12KB의 이모지 선택기에 어떤 영향을 미칠지 궁금했습니다.
하지만 며칠을 고민하다 CSS content-visibility
을 이용하면 어떨까 하는 생각이 들었습니다. 트레이스에서 레이아웃과 페인트에 많은 시간이 소요되는 것을 보니 "버벅거림(stuttering)" 개선에 도움이 될 수 있을 것 같았습니다. 이것은 완전한 가상화보다 훨씬 간단한 솔루션이 될 수 있습니다.
어쩌면 여러분께 생소할 수 있는 content-visibility
는 레이아웃과 페인트의 관점에서 DOM의 특정 부분을 “숨길” 수 있는 새로운 CSS 기능입니다. 접근성 트리에는 크게 영향을 미치지 않으며(DOM 노드는 여전히 존재하므로), 페이지 내 찾기(⌘+F/Ctrl+F)에도 영향을 미치지 않으므로 가상화도 필요하지 않습니다. 화면 밖 요소의 크기 추정치만 있으면 브라우저가 대신 공간을 예약할 수 있습니다.
다행히도 제게는 크기를 예상하기 좋은 기본 단위 요소인 이모지 카테고리가 있었습니다. 페디버스의 사용자 지정 이모지는 "Blobs", "Cats" 등의 작은 카테고리로 나뉘어져 있습니다.
mastodon.social
의 커스텀 이모지
각 카테고리에 대해 이모지 크기와 행과 열의 개수를 이미 알고 있었기 때문에 CSS 사용자 정의 속성을 사용해 예상되는 크기를 계산할 수 있었습니다.
.category {
content-visibility: auto;
contain-intrinsic-size:
/* width */
calc(var(--num-columns) * var(--total-emoji-size))
/* height */
calc(var(--num-rows) * var(--total-emoji-size));
}
브라우저가 contain-intrinsic-size 속성을 통해 보이지 않는 렌더링 공간을 미리 확보하고, 이 공간은 렌더링 완료된 영역과 크기와 정확하게 일치하므로, 스크롤 중에도 스크롤이 갑자기 이동하는 현상을 방지할 수 있습니다.
다음으로 제가 한 일은 진행 상황을 추적하기 위해 타코미터(Tachometer) 벤치마크를 작성하는 것이었습니다. (저는 타코미터를 좋아합니다.) 이를 통해 실제로 성능이 향상되고 있는지, 얼마나 향상되었는지 확인할 수 있었습니다.
첫 번째 시도는 정말 쉽게 작성할 수 있었고 성능 이득도 있었지만... 조금 실망스러웠습니다.
초기 로드의 경우 Chrome에서는 약 15%, Firefox에서는 5%의 개선이 있었습니다. (Safari는 테크니컬 프리뷰에서만 content-visibility
를 지원하기 때문에 타코미터에서는 테스트할 수 없습니다.) 이는 유의미한 결과이지만, 저는 가상 목록을 통해 성능을 훨씬 더 개선할 수 있다는 것을 알고 있었습니다!
그래서 좀 더 깊이 파고들었습니다. 레이아웃 비용은 거의 사라졌지만 설명할 수 없는 다른 비용이 여전히 남아있었습니다. 예를 들어, Chrome 트레이스에 있는 하나의 큰 덩어리는 무엇일까요?
Chrome이 일부 성능 정보를 "숨기고 있다"고 느껴질 때면 저는 두 가지 중 한 가지를 실행합니다.
chrome:tracing
을 실행하거나 (최근에는) 개발자 도구에서 실험 기능인 “모든 이벤트 표시” 옵션을 활성화합니다.
후자를 택한다면, 표준 Chrome 트레이스보다 조금 더 낮은 수준의 정보를 얻을 수 있지만 완전히 다른 화면이 되진 않습니다. 성능 패널과 chrome:tracing
사이의 꽤 괜찮은 절충안이라고 생각합니다.
그리고 이 상황에서, 저는 곧바로 머릿속을 뒤흔드는 무언가를 발견했습니다.
ResourceFetcher::requestResource
는 도대체 무엇인가요? Chromium 소스 코드를 살펴보지 않고도 직감이 들었습니다. 저 모든 비용은 <img>
때문이 아닐까? 그럴 리가 없죠... 그렇죠? 저는 <img loading=“lazy”>
를 사용하고 있는걸요!
아무튼, 저는 제 직감을 따라 각 <img>
에서 src
를 주석처리 했고, 그러자 그 모든 정체불명의 비용이 사라졌습니다!
Firefox에서도 테스트해 본 결과 역시 크게 개선되었습니다. 그래서 저는 loading=“lazy”
가 제가 생각했던 공짜 점심은 아니라고 믿게 되었습니다.
업데이트: 이 문제에 대해 Chromium에 버그를 신고했습니다. 추가 테스트 결과, 제가 잘못 알고 있었던 것 같습니다. 이 문제는 Chromium에서만 발생하는 문제인 것 같습니다.
이 시점에서 저는 loading=“lazy”
를 제거한다면 40,000개의 DOM 요소를 20,000개로 바꾸는 것이 낫겠다고 생각했습니다. <img>
가 필요하지 않다면, CSS를 사용하여 <button>
의 ::after
의사 요소에 배경 이미지를 설정하는 방법으로 해당 요소를 만드는 시간을 절반으로 줄일 수 있습니다.
.onscreen .custom-emoji::after {
background-image: var(--custom-emoji-background);
}
이 시점에서는 카테고리가 화면 안으로 스크롤될 때 onscreen
클래스를 추가하는 간단한 IntersectionObserver
를 통해 훨씬 더 성능이 좋은 사용자 정의 loading=”lazy“
를 갖게 되었습니다. 이번에는 타코미터가 Chrome에서 약 40%, Firefox에서 약 35% 개선되었다고 보고했습니다. 이제 더 좋아졌습니다!
참고:
IntersectionObserver
대신contentvisibilityautostatechange
이벤트를 사용할 수도 있었지만 브라우저 간 동작의 차이가 있고, 추가로 Safari에서는 해당 이벤트를 지원하지 않기 때문에 모든 이미지를 즉시 다운로드 하도록 강요하여 더 불리할 수 있습니다. 하지만 브라우저가 기능을 안정적으로 지원한다면 꼭 사용하고 싶습니다!
저는 이 솔루션에 대해 만족감을 느끼고 출시했습니다. 벤치마크 결과, 크롬과 파이어폭스 모두에서 최대 45%의 개선이 있었고, 원래의 재현 시간은 최대 3초에서 최대 1.3초로 단축되었습니다. 심지어 버그를 제보한 사람도 이모지 선택기가 훨씬 더 유용해졌다며 저에게 감사의 인사를 전하기도 했습니다.
하지만 여전히 아쉬운 부분이 있습니다. 트레이스를 보면 2만 개의 DOM 노드를 렌더링하는 것이 가상화된 목록만큼 빠를 수 없다는 것을 알 수 있었습니다. 그리고 더 많은 이모티콘으로 더 큰 페디버스 인스턴스를 지원하려면 이 솔루션으로는 확장할 수 없습니다.
하지만 content-visibility
로 "공짜로" 얻는 개선이 인상적이었습니다. ARIA 전략을 전혀 변경하거나 페이지 내 검색에 대해 걱정할 필요가 없다는 사실은 신의 선물같았습니다. 하지만 제 안의 완벽주의자는 여전히 완벽함을 위해서라면 가상 목록이 필요하다는 사실을 짜증나게 외치고 있습니다.
언젠가는 웹 플랫폼에 진짜 가상 목록이 기본으로 제공될 수 있을까요? 몇 년 전에 이를 위한 노력이 있었지만 중단된 것 같습니다.
그 날이 오기를 기대하지만, 지금으로서는 content-visibility
이 가상 목록을 대체할 수 있는 좋은 대안이라는 점을 인정하겠습니다. 구현이 간단하고, 성능도 상당히 향상되며, 접근성 문제도 거의 없습니다. 다만, 저한테 10만 개의 커스텀 이모지를 지원하라고 하진 말아 주세요!
🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!
Utilizing CSS content-visibility can significantly enhance rendering performance, optimizing web applications for a smoother user experience. Just like mastering the Snow Rider game requires strategic moves for better performance, applying content-visibility effectively can streamline elements and reduce load times. By prioritizing what’s visible, developers can ensure a more responsive interface, ultimately engaging users more effectively. https://snowridergame.io
Professionals seeking to enhance their expertise can find industry-relevant learning opportunities. Flexible programs cater to various career stages, offering practical skills for immediate application. Elevate professional development with UNICCM. https://www.uniccm.com/