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

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

작성일
2024년 11월 04일
태그
카테고리
기타정리
Last edited time
Last updated November 8, 2024
날짜
이 글은 웹 성능 최적화를 위해 **CSS의 content-visibility**와 contain-intrinsic-size 속성을 활용하여 DOM의 많은 요소를 효율적으로 렌더링하는 방법을 설명하고 있습니다. 특히, 대량의 이모지를 사용하는 상황에서 브라우저의 렌더링 부담을 줄이고자 이모지 선택기의 성능을 개선하는 과정에 초점을 맞추고 있습니다.
 

세부 내용 요약

  1. 문제 상황:
      • 이모지 선택기에는 수만 개의 <button><img> 요소가 포함되어 있어 DOM의 요소 수가 많아 초기 로딩과 스크롤 성능이 매우 저하되었습니다.
      • <img loading="lazy">를 사용하여 이미지가 화면에 나타날 때 로드되도록 했으나, DOM에 수많은 요소가 있는 상황에서는 이 방법만으로는 한계가 있었습니다.
  1. loading="lazy"로 이미지 지연 로딩:
      • <img loading="lazy"> 속성을 사용해 브라우저가 이미지를 화면에 표시할 때만 로드하도록 설정했습니다.
      • 이를 통해 이미지가 한 번에 모두 다운로드되는 것을 방지할 수 있었지만, 여전히 전체 DOM에 존재하는 많은 <img> 태그가 성능에 영향을 미쳤습니다. 이로 인해 브라우저가 모든 이미지에 대해 리소스를 확인하고 관리해야 했기 때문입니다.
  1. content-visibility: auto로 렌더링 효율화:
      • CSS의 content-visibility: auto 속성을 사용하여 화면에 보이지 않는 요소의 렌더링을 지연시켰습니다.
      • 이 속성을 사용하자, 보이지 않는 요소는 페인트와 레이아웃 계산이 생략되어 초기 로딩 속도가 크게 향상되었습니다.
      • content-visibility는 특히 스크롤을 통해 콘텐츠가 동적으로 표시되는 경우에 유용하여, 페이지의 렌더링 성능을 개선할 수 있었습니다.
  1. contain-intrinsic-size로 레이아웃 안정성 유지:
      • content-visibility로 인해 보이지 않는 요소의 크기가 할당되지 않아 레이아웃이 어색하게 보일 수 있습니다. 이를 보완하기 위해 contain-intrinsic-size 속성을 사용하여 예상 크기를 미리 지정했습니다.
      • 각 이모지 카테고리의 크기를 고정 값으로 지정하여 스크롤 시 레이아웃이 흔들리지 않도록 했습니다.
  1. <img> 태그 제거와 CSS 대체:
      • 최적화의 마지막 단계로, <img> 태그 자체를 줄여 성능 문제를 해결하려고 했습니다. 각 이모지의 이미지를 <button>::after 의사 요소에 배경 이미지로 설정하여 불필요한 <img> 요소를 없앴습니다.
      • CSS로 대체하면서 DOM 요소 수가 절반으로 줄었고, 브라우저가 관리해야 할 요소 수가 줄어들어 렌더링 효율이 크게 개선되었습니다.
  1. 성능 테스트와 결과:
      • Chrome에서 15%, Firefox에서 5% 성능 개선을 확인했습니다.
      • 최종적으로 <img loading="lazy"> 속성이 완벽한 해결책이 아닐 수 있으며, 브라우저에 따라 다른 성능 이슈가 발생할 수 있음을 확인했습니다. 특히, Chromium에서 loading="lazy"의 리소스 요청 관리 과정이 성능에 영향을 미쳤습니다.

결론

이 글은 loading="lazy"가 대규모 DOM 요소의 성능 문제를 완전히 해결하지 못할 수 있다는 점을 강조하며, CSS 속성으로 렌더링을 지연하고 <img> 태그를 CSS 배경 이미지로 대체하는 방식을 통해 성능을 최적화하는 방법을 제시합니다.
 
 
 
 
 
참고 구현
import { useEffect, useRef } from 'react'; import EmojiComponent from './EmojiComponent'; export default function EmojiContainer() { const emojiContainerRef = useRef(null); const emojiUrls = [ '/path/to/emoji1.png', '/path/to/emoji2.png', '/path/to/emoji3.png', // 추가 이모지 경로들... ]; useEffect(() => { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const target = entry.target; const emojiUrl = target.getAttribute('data-emoji-url'); // CSS 변수로 배경 이미지 설정 target.style.setProperty('--custom-emoji-background', `url(${emojiUrl})`); target.classList.add('onscreen'); // onscreen 클래스 추가 observer.unobserve(target); // 한 번 로드 후 관찰 해제 } }); }); // 자식 요소들을 한 번에 관찰 시작 const children = emojiContainerRef.current.querySelectorAll('.custom-emoji'); children.forEach((child) => observer.observe(child)); // 컴포넌트 언마운트 시 Observer 해제 return () => { children.forEach((child) => observer.unobserve(child)); }; }, []); return ( <div ref={emojiContainerRef} className="emoji-container"> {emojiUrls.map((url, index) => ( <EmojiComponent key={index} emojiUrl={url} /> ))} </div> ); } export default function EmojiComponent({ emojiUrl }) { return ( <button className="custom-emoji" data-emoji-url={emojiUrl} > {/* 내용은 비워두고 ::after로 배경 이미지 설정 */} </button> ); }
 
.custom-emoji { width: 50px; height: 50px; margin: 5px; position: relative; } .custom-emoji::after { content: ""; display: block; width: 100%; height: 100%; background-size: cover; background-position: center; background-repeat: no-repeat; } .custom-emoji.onscreen::after { background-image: var(--custom-emoji-background); }