우리는 DOM이 XML 및 HTML 문서를 운영하기 위한 애플리케이션 프로그래밍 인터페이스이며, DOM을 운영하기 위해 스크립트를 사용하는 데 비용이 매우 많이 든다는 것을 알고 있습니다. 적절한 비유가 있습니다. DOM과 JavaScript(여기서는 ECMScript)가 각각 하나의 섬이고 유료 다리로 연결되어 있다고 상상해 보세요. ECMAScript가 DOM에 액세스할 때마다 이 다리를 통과해야 하며 "다리 통행료"를 지불해야 합니다. DOM에 액세스하는 횟수가 많을수록 비용이 높아집니다. 따라서 권장되는 접근 방식은 가능한 한 적은 수의 다리를 건너고 ECMAScript 섬에 머무르는 것입니다. DOM 인터페이스를 사용하지 않는 것은 불가능합니다. 그렇다면 어떻게 프로그램의 효율성을 높일 수 있을까요?
1. DOM 액세스 및 수정
DOM 요소에 액세스하는 것은 비용이 많이 들고("톨"을 알고 있음) 요소를 수정하면 브라우저가 페이지의 기하학적 변경 사항(리플로우 및 다시 그리기)을 다시 계산하게 되므로 비용이 훨씬 더 많이 듭니다.
물론 최악의 시나리오는 루프의 요소에 액세스하거나 수정하는 것입니다. 다음 두 코드를 살펴보세요.
var times = 15000; // code1 console.time(1); for(var i = 0; i < times; i++) { document.getElementById('myDiv1').innerHTML += 'a'; } console.timeEnd(1); // code2 console.time(2); var str = ''; for(var i = 0; i < times; i++) { str += 'a'; } document.getElementById('myDiv2').innerHTML = str; console.timeEnd(2);
그 결과, 두 번째보다 첫 번째 러닝타임이 천 배나 길어졌습니다! (크롬 버전 44.0.2403.130m)
1: 2846.700ms 2: 1.046ms
첫 번째 코드 조각의 문제점은 루프가 반복될 때마다 요소에 두 번 액세스된다는 것입니다. 한 번은 innerHTML 값을 읽고, 다른 한 번은 이를 다시 작성하기 위해, 즉 루프가 브리지를 교차할 때마다( 다시 로잉하고 다시 그리는 방법은 다음 글에서 설명하겠습니다! 결과는 DOM에 더 많이 액세스할수록 코드 실행 속도가 느려진다는 것을 분명히 보여줍니다. 따라서 줄일 수 있는 DOM 액세스 횟수를 최대한 줄이고 처리는 최대한 ECMAScript 측에 맡깁니다.
2. HTML 수집 및 DOM 탐색
DOM 운영의 또 다른 에너지 소모 지점은 DOM을 순회하는 것입니다. 일반적으로 우리는 getElementsByTagName() 또는 document.links 등을 사용하여 HTML 모음을 수집하게 됩니다. 이는 모두가 익숙할 것이라고 생각합니다. 컬렉션의 결과는 실시간으로 "라이브 상태"로 존재하는 배열과 유사한 컬렉션입니다. 즉, 기본 문서 개체가 업데이트될 때 자동으로 업데이트됩니다. 어떻게 말하나요? 밤을 주는 방법은 매우 간단합니다.
<body> <ul id='fruit'> <li> apple </li> <li> orange </li> <li> banana </li> </ul> </body> <script type="text/javascript"> var lis = document.getElementsByTagName('li'); var peach = document.createElement('li'); peach.innerHTML = 'peach'; document.getElementById('fruit').appendChild(peach); console.log(lis.length); // 4 </script>
여기서 비효율성이 발생합니다! 매우 간단합니다. 배열의 최적화 작업과 마찬가지로 길이 변수를 캐싱하는 것도 괜찮습니다(컬렉션의 길이를 읽는 것은 매번 쿼리해야 하기 때문에 일반 배열의 길이를 읽는 것보다 훨씬 느립니다).
console.time(0); var lis0 = document.getElementsByTagName('li'); var str0 = ''; for(var i = 0; i < lis0.length; i++) { str0 += lis0[i].innerHTML; } console.timeEnd(0); console.time(1); var lis1 = document.getElementsByTagName('li'); var str1 = ''; for(var i = 0, len = lis1.length; i < len; i++) { str1 += lis1[i].innerHTML; } console.timeEnd(1);
얼마나 성능 향상이 가능한지 살펴볼까요?
0: 0.974ms 1: 0.664ms
컬렉션의 길이가 길어도(데모는 1000) 성능 향상은 여전히 뚜렷합니다.
"고성능 JavaScript"는 "배열을 순회하는 것이 컬렉션을 순회하는 것보다 빠르기 때문에 컬렉션 요소를 배열에 먼저 복사하면 해당 속성에 액세스하는 것이 더 빨라진다"는 또 다른 최적화 전략을 제안합니다. 이 패턴은 잘 공개되지 않았으니 신경쓰지 마세요. 테스트 코드는 다음과 같습니다. (궁금한 점이 있으시면 언제든지 저에게 문의해주세요.)
console.time(1); var lis1 = document.getElementsByTagName('li'); var str1 = ''; for(var i = 0, len = lis1.length; i < len; i++) { str1 += lis1[i].innerHTML; } console.timeEnd(1); console.time(2); var lis2 = document.getElementsByTagName('li'); var a = []; for(var i = 0, len = lis2.length; i < len; i++) a[i] = lis2[i]; var str2 = ''; for(var i = 0, len = a.length; i < len; i++) { str2 += a[i].innerHTML; } console.timeEnd(2);
이 섹션의 마지막에는 querySelector() 및 querySelectorAll()이라는 두 가지 기본 DOM 메서드를 소개합니다. 전자는 배열을 반환합니다(반환 값은 동적으로 변경되지 않습니다). HTML 컬렉션과 유사) 후자는 일치하는 첫 번째 요소를 반환합니다. 글쎄, 항상 이전의 HTML 컬렉션 순회보다 더 나은 성능을 발휘하는 것은 아닙니다.
console.time(1); var lis1 = document.getElementsByTagName('li'); console.timeEnd(1); console.time(2); var lis2 = document.querySelectorAll('li'); console.timeEnd(2); // 1: 0.038ms // 2: 3.957ms
하지만 CSS와 같은 선택 방식이기 때문에 조합 선택을 할 때 더 효율적이고 편리합니다. 예를 들어 다음과 같은 결합된 쿼리를 수행합니다.
var elements = document.querySelectorAll('#menu a'); var elements = document.querySelectorAll('div.warning, div.notice');
위 내용은 모두 고성능 JavaScript DOM 프로그래밍에 관한 내용이므로 이해하고 학습에 도움이 되기를 바랍니다.