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


