Lighthouse로 사이트의 성능을 측정해보면 과도한 DOM 크기 지양하기 라는 경고가 표시되는 것을 보신 적이 있으실겁니다.
DOM 크기가 크면 메모리 사용량이 증가하고 스타일 계산에 많은 비용이 발생할 수 있기 때문에 Lighthouse에서 경고하는 것입니다. 페이지에서 진행되는 다른 모든 문제들과 결합될 때, 이는 사용자 경험에 더 큰 영향을 미칠 수 있으며, 특히 저사양 디바이스 사용자에게는 더욱 그렇습니다.
얼마 전 제 사이트에 대한 성능 보고서를 읽고 있었는데 이 경고가 제 눈길을 끌었습니다. 하지만 눈길을 끈 것은 총 DOM 요소의 갯수가 아닌 그 아래에 보고된 메트릭, 즉 최대 DOM 깊이였습니다.
지난주 뉴스레터를 작성하면서 트리(숲에서 자라는 나무가 아닌 자료구조)에 대해 깊이 생각하는 시간을 가졌기 때문에 시간 복잡성과 트리의 깊이 사이의 관계가 제 머릿속에는 생생하게 남아있었습니다. 그래서 Lighthouse 보고서의 지표를 보고 바로 이런 의문이 생겼습니다.
DOM과 같은 트리 구조로 작업할 때 트리의 깊이는 조회 같은 작업들의 연산 속도와 많은 관련이 있습니다. 다음 두 개의 DOM 트리를 살펴보겠습니다.
얕은 DOM 트리와 깊은 DOM 트리입니다. 둘 다 요소의 수는 동일합니다.
두 트리 모두 동일한 수의 요소를 갖고 있습니다. 그러나 한 쪽은 깊이(혹은 높이) 2로 이루어져 있고, 다른 한 쪽은 깊이 6으로 이루어져 있습니다. 트리가 더 깊어질수록 요소에 접근하는 데 더 많은 연산이 필요할 수 있기 때문에 이 차이가 중요합니다.
예를 들어, 트리의 루트로부터 <img>
요소에 접근하고 싶다고 가정해봅시다. 얕은 트리의 경우에는 두 번의 작업만 수행하면 됩니다. <body>
의 자식 배열을 찾고 인덱스 4에 위치한 자식에 접근하면 됩니다.
body.children[4];
하지만 깊은 트리에서는 같은 요소에 도달하기 위해 6번 점프해야 합니다.
body.children[0].children[0].children[0].children[0].children[0];
이진 검색 트리(Binary Search Trees)와 같은 특정 유형의 트리를 다룰 때는 트리의 깊이가 특히 중요합니다. 트리의 노드가 많아짐에 따라서 트리의 깊이를 최소한으로 유지하고 조회와 같은 작업을 최대한 빠르게 수행할 수 있도록 자가 균형 이진 검색 트리를 구현하는 다양한 자료 구조가 존재하는 이유도 바로 이것 때문입니다.
다시 DOM으로 돌아가 보겠습니다. 이론적으로는 트리가 깊어질수록 속도가 느려진다는 것을 알고 있습니다. 하지만 실제로는 성능에 어떤 영향을 미칠까요?
이를 알아보기 위해 간단한 실험을 해보겠습니다.
여기에 보고된 모든 메트릭은 M1 Max MacBook에서 CPU 속도를 4배 느리게 한 Google Chrome 125(시크릿 모드)를 사용하여 성능 프로파일링을 실행한 결과입니다. 테스트 페이지의 소스 코드는 GitHub에서 확인하실 수 있습니다.
이를 테스트하기 위해 세 줄의 텍스트와 100개의 빈 div만 포함된 HTML 페이지 두 개를 만들어 보았습니다. 두 페이지의 유일한 차이점은 한 페이지에는 모든 div가 document의 body에 직접 있는 반면, 다른 페이지에는 div가 중첩되어 있다는 것입니다.
따라서 얕은 요소 트리를 가진 페이지는 아래와 같은 구조를 가집니다.
<html>
<body>
<div></div>
<div></div>
<div></div>
<div></div>
<!-- 95 div 생략... -->
<div>마지막 100번째 div.</div>
</body>
</html>
깊은 요소 트리를 가진 페이지는 아래와 같은 구조를 가집니다.
<html>
<body>
<div>
<div>
<div>
<div>
<!-- 95 div 생략... -->
<div>마지막 100번째 div.</div>
</div>
</div>
</div>
</div>
</body>
</html>
첫 번째 페이지에서 성능 프로파일링을 실행한 결과 총 페이지 로드 시간(파싱 + 렌더링 + 페인팅)이 51밀리초로 측정되었습니다. 두 번째 페이지에서는 페이지 로드 시간이 무려.. 53밀리초였습니다.
약간 실망스러운 결과였습니다.
물론 모든 div가 비어 있고 브라우저가 화면에 몇 줄의 텍스트만 렌더링하고 있기 때문에 큰 의미가 있는 예는 아니지만, 그래도 더 큰 차이를 기대했었습니다.
다행히도 프로그래밍의 마법 덕분에 몇 번의 키 입력만으로 더 많은 수의 div로 테스트할 수 있었습니다. 그래서 200개, 300개, 400개로 테스트해보았고, 500개의 div에서는 성능에 분명한 차이가 있었습니다.
500개의 중첩된 div가 있는 페이지의 로드 속도가 100ms가 조금 넘는 것으로 확인된 성능 보고서.
위의 얕은 트리에서는 처리해야 하는 HTML 양이 5배 증가해도 거의 영향을 받지 않고 500개의 div를 모두 렌더링하는 데에 56ms가 걸렸습니다. 하지만 중첩된 트리의 경우 거의 두 배에 달하는 102ms가 걸렸습니다.
그래서 계속 테스트를 진행했고 최대 5,000개의 div로 테스트해보았습니다. 결과는 다음과 같습니다.
여기서 주목할 점은 얕은 트리가 있는 페이지와 중첩된 트리가 있는 페이지의 크기는 모두 동일하다는 것입니다. 둘 다 동일한 양의 요소를 포함하며 브라우저에서 렌더링될 때 정확히 동일하게 보입니다.
렌더링 성능에 영향을 미치는 유일한 차이점은 위의 차트에서 볼 수 있듯이 DOM 트리의 깊이입니다.
실제 웹 사이트는 그렇게 나쁠 리 없으니 5,000의 DOM 깊이까지 고려할 필요는 없다고 생각할 수도 있습니다... 그리고 그 말이 맞습니다.
하지만 사실 5,000개의 요소는 그리 드문 일이 아니며 실제 웹사이트에서는 모든 요소가 얕은 트리 예시에서와 같은 수준의 깊이를 갖지 않을 것입니다.
깊이가 32로 비교적 낮더라도 그 많은 요소를 파싱, 렌더링 및 페인팅하는 데에는 수백 밀리초가 걸리며, 심지어 이 모든 것은 CSS와 JS 리소스 작업이 상황을 훨씬 더 악화시키기 전의 일입니다.
CSS 얘기가 나와서 이야기해보자면, 크고 깊은 DOM 트리가 문제가 될 수 있는 이유 중 하나는 스타일 재계산을 위한 비용이 많이 들기 때문입니다.
5,000개의 중첩된 div를 예로 들어 각 div에 약간의 텍스트를 추가하면 단 하나의 CSS 규칙만 추가해서 스타일 재계산의 영향도를 테스트할 수 있습니다.
div {
padding-top: 10px;
}
이 규칙은 페이지의 거의 모든 요소에 영향을 미치기 때문에 브라우저는 각 div의 업데이트된 위치를 파악하는 데 오랜 시간을 소비해야 합니다. 이렇게 많은 시간이 소요되는 스타일 재계산 작업은 메인 스레드를 차단하여 사이트가 응답하지 못하게 만들 수 있습니다.
페이지의 거의 모든 요소에 영향을 미치는 값비싼 스타일 재계산
이 실험에서는 페이지 로드 성능만 측정하지만 사용자는 초기 로드 이후에도 페이지와 상호 작용한다는 점을 염두에 두어야 합니다.
그렇기 때문에 DOM 크기와 깊이를 계속 주시하는 것은 매우 중요합니다. 사이트 로딩 속도가 느려지기 때문이 아니라 다른 모든 런타임 작업(예: 사용자 상호 작용에 의한 자바스크립트를 통한 DOM 업데이트)이 수행될 기준선이 되기 때문입니다.
그렇다면 이 문제를 어떻게 해결할 수 있을까요?
가장 중요한 것은 주기적으로 DOM 크기와 깊이를 확인하는 것입니다. 당연하게 들리겠지만, 일반적으로 한 번에 약간의 HTML만 노출되는 UI 컴포넌트나 템플릿의 일부분으로 작업하기 때문에 이러한 마크업이 어떻게 추가되는지 잊어버리기 쉽습니다.
Lighthouse 또는 PageSpeed Insights와 같은 도구를 사용하여 사이트의 DOM 크기와 깊이를 측정할 수 있습니다. 특정 시간에 페이지에 얼마나 많은 요소가 있는지 빠르게 확인하려면 브라우저의 콘솔에서 아래를 실행하면 됩니다.
document.querySelectorAll("*").length;
CSS 선택자의 범위와 복잡성을 줄이는 것도 큰 도움이 됩니다. 이렇게 하면 브라우저는 타겟팅하려는 요소를 더 쉽게 찾을 수 있고 스타일 재계산을 훨씬 빠르게 수행할 수 있습니다.
페이지의 요소 수가 400에서 6,000으로 증가하면 전환 확률이 95% 감소함
큰 DOM 크기가 전환율에 미치는 영향을 보여주는 2017년 연구의 통계입니다.
이 작은 실험에서 두 가지 주요한 시사점을 얻었습니다.
첫 번째는 최신 브라우저는 놀랍다는 것입니다. 수천 개의 레벨이 중첩된 DOM 트리를 단 몇 밀리초 만에 파싱, 렌더링 및 페인팅할 수 있다는 사실은 정말 놀랍습니다.
앞서 언급했듯이 실제로 DOM의 깊이가 5,000인 경우는 별로 없지만, 어쨌든 브라우저(적어도 제가 테스트한 Chrome)가 부하를 처리하도록 최적화되어 있다는 점은 정말 멋집니다.
두 번째 교훈은 DOM 크기와 깊이가 사이트 성능에 미치는 영향이 생각보다 크다는 것인데, 특히 스타일 재계산과 결합할 경우 그 영향은 더 커집니다.
값비싼 자바스크립트 연산만큼 영향력이 크지는 않지만, 분명 성능상 차이를 만들 수 있고 DOM 요소는 빠르게 추가되므로 계속 주시할 가치가 있습니다.
이 주제에 대한 자세한 내용은 다음 내용들을 참조하세요.
잘 읽었습니다 👍🏻