이 글은 원저자 Nicholas Ray의 허락을 받아 300ms Faster: Reducing Wikipedia's Total Blocking Time을 한국어로 번역한 글입니다. :)
클릭했는데 느리게 반응하거나 스크롤이 끊기는 웹사이트로 인해 불편을 겪은 적이 있으신가요? 이러한 성능 결함으로 다음과 같은 문제가 발생할 수 있습니다.
3년 넘게 위키피디아 모바일 사이트는 저사양의 디바이스에서 페이지를 로드 중에 실행이 600밀리 초 이상 소요되는 자바스크립트 문제로 고생했습니다. 이로 인해 낮은 성능의 기기에서는 사용자의 상호작용이 사실상 원활하게 이루어지기 어려웠습니다.
이 글에서는 제가 해당 작업의 실행 시간을 약 50% 감소시키기 위해 취한 몇 가지 방법들을 소개해 보겠습니다.
600밀리 초의 동기식 자바스크립트 실행은 그리 길지 않은 시간처럼 들릴 수 있지만, 페이지 로딩 중에 자바스크립트가 실행되기 시작할 때 사용자가 버튼을 클릭하려는 상황을 상상해 보세요. 브라우저의 메인 스레드에서는 한 번에 하나의 작업만 처리될 수 있기 때문에 사용자는 다음 일련의 단계들이 완료되어야 시각적으로 업데이트를 확인할 수 있습니다.
앞의 작업(1)이 길어지면 시각적 업데이트를 생성하는 클릭 핸들러의 실행이 지연될 수 있습니다.
각 단계는 시간을 소요하고, 사용자는 시각적으로 업데이트하는 데 100밀리 초보다 오래 걸리는 모든 인터랙션을 느리다고 인식할 수 있습니다. 따라서 구글은 50밀리 초 이상 걸리는 모든 작업을 "긴 작업"으로 간주하며, 이러한 작업들은 사용자 입력에 대한 페이지의 반응성에 영향을 미칠 수 있습니다. 이를 측정하는 "총 차단 시간"(TBT)이라 불리는 지표가 개발되었습니다.
50밀리 초 이상 걸리는 작업이 두 가지(80밀리 초와 100밀리 초) 있습니다.
TBT는 First Contentful Paint(FCP)와 Time to Interactive(TTI) 사이에 브라우저 메인 스레드에서 오래 걸리는 작업들에 의해 차단되는 부분의 합을 측정합니다. "차단되는 부분"이란 각 오래 걸리는 작업의 첫 50밀리 초 이후의 시간을 의미합니다.
아래 예시에서 TBT를 계산해봅시다.
TBT는 각 오래 걸리는 작업들이 50밀리 초보다 초과하여 소요하는 시간의 합이므로, 위 예시에서 TBT는 30밀리 초와 50밀리 초를 더해 80밀리 초가 됩니다.
평균적인 모바일 하드웨어에서 테스트한 결과에 기반하여, 구글은 웹사이트의 TBT가 200밀리 초를 초과하지 않는 것을 권장하고 있습니다. 그러나 위키피디아는 단 하나의 작업만으로도 구글에서 권장하는 TBT 한도의 약 3배 이상인 600밀리 초를 소요했습니다.
TBT를 어떻게 개선할 수 있을까요?
TBT를 줄이기 위해서는 다음과 같은 항목을 따라야 합니다.
이 글에서는 첫 번째 항목에 초점을 맞추어 TBT를 개선합니다.
메인 스레드에서 HTML 파싱, 페인트, 가비지 컬렉션 등 여러 작업이 실행되지만, TBT 문제의 주요 원인은 긴 자바스크립트 실행인 경우가 많습니다. 즉, 자바스크립트는 웹사이트 속도를 저하시키는 가장 빠른 방법입니다.
위키피디아의 모바일 웹사이트를 프로파일링 한 결과, _enable
메서드가 실행 시간의 대부분을 차지한다는 것을 발견했습니다. 이 메서드는 모바일 웹사이트의 섹션을 확장/축소하는 동작을 초기화합니다. 또한 _enable
메서드 내에서 jQuery의 .on("click")
메서드의 호출이 느리다는 것을 프로파일링 결과에서 알 수 있었습니다.
function _enable( $container, prefix, page, isClosed ) {
...
// 편집자가 생성한 링크로 제한되어 있으므로 우리의 제어를 벗어납니다
// T166544 - 참조 링크에 대해 이 작업을 수행하지 마세요
// 참조 링크는 다른 곳에서 처리됩니다
var $link = $container.find("a:not(.reference a)");
$link.on("click", function () {
// 링크는 해시와 포함된 내부 링크일 수 있습니다
// 해시가 포함된 경우, 어떤 섹션을 보여주어야 하는지 확인합니다
if (
$link.attr("href") !== undefined &&
$link.attr("href").indexOf("#") > -1
) {
checkHash();
}
});
util.getWindow().on("hashchange", function () {
checkHash();
});
}
.on("click")
호출은 콘텐츠의 거의 모든 링크에 클릭 이벤트 리스너를 부착하여 클릭한 링크에 해시 프래그먼트가 포함된 경우 해당 섹션이 열리도록 했습니다. 링크가 거의 없는 짧은 기사의 경우 성능에 미치는 영향은 미미했습니다. 하지만 "미국"과 같은 긴 기사에는 4천 개 이상의 링크가 포함되어 있어 저사양 기기에서 200밀리 초 이상의 실행 시간을 초래했습니다.
더 심각한 것은 이 동작이 불필요했다는 점입니다. hashchange
이벤트를 구독하는 코드가 이미 클릭 이벤트 리스너가 호출한 것과 동일한 메서드를 호출하고 있었습니다. window의 location이 이미 링크의 대상을 가리키고 있지 않는 한, 링크를 클릭하면 checkHash
메서드가 클릭 이벤트 핸들러와 hashchange
핸들러에서 각각 한 번씩, 총 두 번 호출되었습니다.
따라서 이 경우에 가장 좋은 접근 방식은 이 자바스크립트 블록을 제거하고 거의 동일한 기능을 유지하면서 메인 스레드에서 약 200밀리 초를 확보하는 것이었습니다.
프로파일링 할 때는 항상 가장 많은 시간이 소요되는 곳을 확인하세요. 그런 다음 최적화하거나 더 나은 방법으로 제거할 수 있는 코드가 있는지 확인하세요.
웹사이트 속도를 높이는 가장 빠른 방법은 자바스크립트를 제거하는 것임을 기억하세요.
추가적으로 성능을 검토한 결과, initMediaViewer
메서드가 실행되는 데 약 100밀리 초가 걸리는 것으로 나타났습니다. 이 메서드는 콘텐츠의 각 섬네일에 클릭 이벤트 리스너를 연결하여 섬네일을 클릭했을 때 미디어 뷰어가 열리도록 했습니다.
/**
* 이미지 섬네일 클릭에 대한 이벤트 핸들러
*
* @param {jQuery.Event} ev
* @ignore
*/
function onClickImage(ev) {
ev.preventDefault();
routeThumbnail($(this).data("thumb"));
}
/**
* 이미지에 경로를 추가하고 클릭을 제어
*
* @method
* @ignore
* @param {jQuery.Object} [$container] 검색할 옵셔널 한 컨테이너
*/
function initMediaViewer($container) {
currentPageHTMLParser
.getThumbnails($container)
.forEach(function (thumb) {
thumb.$el.off().data("thumb", thumb).on("click", onClickImage);
});
}
1단계의 링크 예시와 마찬가지로, 각 섬네일에 이벤트 리스너를 연결하는 것은 확장성이 떨어집니다. 위키백과 문서 편집자들은 수천 개의 이미지가 포함된 문서를 만들 수 있으며 실제로 그렇게 생성되고 있습니다. 이 코드 블록을 실행하면 이미지가 많은 페이지에서 실행하는 데 100밀리 초보다 훨씬 오래 걸리고 페이지의 TBT가 증가할 수 있습니다. 이 문제에 대한 다른 접근 방식은 무엇이 있을까요?
바로 이벤트 위임을 활용하는 것입니다.
이벤트 위임은 여러 요소의 공통되는 상위 요소에 이벤트 리스너 하나만을 연결하는 강력한 기술입니다. 여러 요소를 추가할 수 있는 사용자 생성 콘텐츠를 다룰 때는 이벤트 위임을 사용하는 것이 더 효율적인 경우가 많습니다. 이 기술은 이벤트 버블링을 활용하여 다음과 같이 동작합니다.
event
파라미터를 사용하여, 이벤트의 출처를 찾기 위해 event.target
프로퍼티를 확인합니다. 상위 요소를 확인하기 위해 event.target.closest(selector)
API를 사용할 수도 있습니다.변경된 코드는 다음과 같습니다.
/**
* 이미지 섬네일 클릭에 대한 이벤트 핸들러
*
* @param {MouseEvent} ev
* @ignore
*/
function onClickImage(ev) {
var el = ev.target.closest(PageHTMLParser.THUMB_SELECTOR);
if (!el) {
return;
}
var thumb = currentPageHTMLParser.getThumbnail($(el));
if (!thumb) {
return;
}
ev.preventDefault();
routeThumbnail(thumb);
}
/**
* 이미지에 경로를 추가하고 클릭을 제어
*
* @method
* @ignore
* @param {HTMLElement} container 검색할 컨테이너
*/
function initMediaViewer(container) {
container.addEventListener("click", onClickImage);
}
이 경우에서 제가 수정한 것을 정리하면 다음과 같습니다.
initMediaViewer
메서드를 수정했습니다.onClickImage
메서드에서 ev.target.closest(selector)
API를 사용하여 클릭 이벤트가 섬네일이나 섬네일 요소의 하위 요소에서 발생한 것인지 확인했습니다. 섬네일에서 발생한 경우면 이벤트를 처리하고, 그렇지 않은 경우 얼리 리턴을 실행합니다.이 수정 작업으로 인한 결과는 어땠을까요?
위에서 1단계, 2단계로 설명한 최적화를 단계별로 두 번의 배포를 통해 프로덕션 환경에 적용했습니다.
위키피디아의 합성 성능 테스트 데이터에 따르면, 실제 Moto G(5) 휴대전화에서 테스트 해보니 첫 번째 배포에서는 TBT가 약 200밀리 초 감소했으며, 두 번째 배포에서는 약 80밀리 초 개선되었습니다. 전반적으로 이 두 단계를 통해 Moto G(5) 휴대전화와 같은 기기에서 긴 기사를 방문했을 때 TBT가 300밀리 초 가까이 감소했습니다.
영어 위키피디아의 "스웨덴" 문서를 방문했을 때, Moto G(5) 기기에서의 위키피디아의 합성 성능 테스트 결과
위키피디아의 실제 유저 모니터링(RUM) 결과를 통해서도, 오래 걸리는 작업의 시간이 개선되었다는 것을 알 수 있었습니다.
p95 인도 사용자의 실제 사용자 모니터링 그래프가 로드 이벤트 전에 발생하는 긴 작업의 시간이 대략 200밀리 초 개선되었음을 보여줍니다.
저사양 기기에서는 여전히 권장 한도를 초과하는 작업이 있지만, 지금까지 이루어진 진전은 상당한 수준입니다. TBT를 더 크게 줄이려면 지금의 작업을 더 작은 작업으로 쪼개야 할 수도 있습니다.
이 경험을 통해 알 수 있는 것은 작고 특정한 최적화를 통해 상당한 성능 향상을 달성할 수 있다는 것입니다. 코드의 특정 부분을 제거하거나 최적화함으로써, 사소한 변화가 웹사이트의 전반적인 성능에 상당한 영향을 미칠 수 있습니다. 이는 모든 기기에서 작동하는 민첩한 브라우징 환경을 제공하는 것이 항상 복잡하고 광범위한 코드 베이스 변경이 필요한 것은 아니라는 점을 상기시켜 줍니다. 때로는 작은 변화가 가장 큰 차이를 만들기도 합니다.