(번역) CSS content-visibility를 이용해 렌더링 성능 향상 시키기

Chanhee Kim·2024년 11월 3일
38

FE 글 번역

목록 보기
27/27
post-thumbnail

원문: https://nolanlawson.com/2024/09/18/improving-rendering-performance-with-css-content-visibility/?utm_source=CSS-Weekly&utm_campaign=Issue-594&utm_medium=web

최근에 emoji-picker-element에서 흥미로운 성능 이슈가 발견되었습니다.

19만 개의 맞춤 이모지가 있는 페디 인스턴스를 사용 중인데 ... 이모지 선택기를 열면 페이지가 최소 1초 동안 멈추고 그 후에도 한동안 전반적인 성능이 악화됩니다.

마스토돈이나 페디버스(Fediverse)가 낯서실 수 있는데요. 이 서비스들은 서버마다 Slack, Discord 등과 같이 자체 사용자 지정 이모지가 있을 수 있습니다. 19,000개(이 경우 실제로는 20,000개에 가깝습니다)는 매우 드문 경우이지만, 전례가 없는 것은 아닙니다.

자, 그래서 버그 제보자의 문제 상황을 재현해봤는데...맙소사, 정말 느렸어요.

이모지 선택기가 있는 Chrome 개발자 도구의 스크린샷으로 높은 레이아웃/페인트 비용과 40,000개의 DOM 노드를 보여줌

여기에는 여러 가지 문제가 있었습니다.

  • 2만 개의 사용자 지정 이모지는 각각 <button><img>를 필요로 하기 때문에 4만 개의 요소가 있음을 의미했습니다.
  • 가상화를 사용하지 않았기 때문에 이러한 모든 요소를 DOM에 그냥 밀어 넣었습니다.

다행히도 저는 <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" 등의 작은 카테고리로 나뉘어져 있습니다.

각각 다른 수의 이모지가 있지만 모두 8개의 열이 그리드에 있는 Blob과 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이 일부 성능 정보를 "숨기고 있다"고 느껴질 때면 저는 두 가지 중 한 가지를 실행합니다.

chrome:tracing을 실행하거나 (최근에는) 개발자 도구에서 실험 기능인 “모든 이벤트 표시” 옵션을 활성화합니다.

후자를 택한다면, 표준 Chrome 트레이스보다 조금 더 낮은 수준의 정보를 얻을 수 있지만 완전히 다른 화면이 되진 않습니다. 성능 패널과 chrome:tracing 사이의 꽤 괜찮은 절충안이라고 생각합니다.

그리고 이 상황에서, 저는 곧바로 머릿속을 뒤흔드는 무언가를 발견했습니다.

이전의 "정체불명의 시간" 대신 ResourceFetcher::requestResource라고 써놓은 Chrome 개발자 도구 스크린샷

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/)을 구독해주세요!

profile
FE 개발을 하고 있어요🌱
post-custom-banner

1개의 댓글

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/

답글 달기