Navigation Timing 및 Resource Timing을 사용하여 현장의 로딩 성능 평가

Navigation 및 Resource Timing API를 사용하여 필드에서 로드 성능을 평가하는 기본사항을 알아봅니다.

브라우저 개발자 도구의 네트워크 패널에서 연결 제한 (또는 Chrome의 경우 Lighthouse)을 사용하여 로드 성능을 평가했다면 이러한 도구가 성능 조정에 얼마나 편리한지 아실 것입니다. 일관되고 안정적인 기준 연결 속도로 성능 최적화의 영향을 빠르게 측정할 수 있습니다. 유일한 문제는 필드 데이터가 아닌 실험실 데이터를 생성하는 합성 테스트라는 것입니다.

합성 테스트는 본질적으로 나쁜 것은 아니지만 실제 사용자를 위해 웹사이트가 로드되는 속도를 나타내지는 않습니다. 그러려면 Navigation Timing 및 Resource Timing API에서 수집할 수 있는 필드 데이터가 필요합니다.

필드의 로드 성능을 평가하는 데 도움이 되는 API

Navigation Timing 및 Resource Timing은 서로 다른 두 가지 항목을 측정하는 상당 부분 겹치는 두 개의 유사한 API입니다.

  • Navigation Timing은 HTML 문서에 대한 요청 (즉, 탐색 요청)의 속도를 측정합니다.
  • Resource Timing은 CSS, JavaScript, 이미지 등과 같은 문서 종속적인 리소스의 요청 속도를 측정합니다.

이러한 API는 브라우저에서 JavaScript를 사용하여 액세스할 수 있는 성능 입력 버퍼에 데이터를 노출합니다. 성능 버퍼를 쿼리하는 방법에는 여러 가지가 있지만 일반적인 방법은 performance.getEntriesByType를 사용하는 것입니다.

// Get Navigation Timing entries:
performance.getEntriesByType('navigation');

// Get Resource Timing entries:
performance.getEntriesByType('resource');

performance.getEntriesByType는 성능 항목 버퍼에서 검색하려는 항목 유형을 설명하는 문자열을 허용합니다. 'navigation''resource'는 각각 Navigation Timing 및 Resource Timing API의 타이밍을 검색합니다.

이러한 API가 제공하는 정보의 양은 압도적일 수 있지만 사용자가 웹사이트를 방문할 때 이러한 시간을 수집할 수 있으므로 현장에서 로드 성능을 측정하는 데 핵심적인 역할을 합니다.

네트워크 요청의 수명 및 타이밍

탐색 및 리소스 타이밍을 수집하고 분석하는 것은 사후에 네트워크 요청의 일시적인 수명을 재구성한다는 점에서 고고학과 같습니다. 개념을 시각화하는 것이 도움이 될 수 있으며 네트워크 요청과 관련된 경우 브라우저의 개발자 도구가 도움이 될 수 있습니다.

Chrome DevTools에 표시된 네트워크 타이밍 다이어그램 요청 큐에 추가, 연결 협상, 요청 자체 및 응답에 대한 시간이 색상으로 구분된 막대로 표시됩니다.
Chrome DevTools의 네트워크 패널에 있는 네트워크 요청 시각화

네트워크 요청의 수명에는 DNS 조회, 연결 설정, TLS 협상 등 서로 다른 단계가 있습니다. 이러한 타이밍은 DOMHighResTimestamp로 표시됩니다. 브라우저에 따라 타이밍의 세밀도는 마이크로초 단위 또는 밀리초로 반올림될 수 있습니다. 이러한 단계를 자세히 살펴보고 Navigation Timing 및 Resource Timing과 이러한 단계 간의 관계를 알아보겠습니다.

DNS 조회

사용자가 URL로 이동하면 도메인을 IP 주소로 변환하기 위해 DNS (도메인 이름 시스템)를 쿼리합니다. 이 과정에는 상당한 시간이 걸릴 수 있으며 현장에서 측정하는 것도 좋습니다. Navigation Timing과 Resource Timing은 두 가지 DNS 관련 타이밍을 노출합니다.

  • domainLookupStart은 DNS 조회가 시작되는 시간입니다.
  • domainLookupEnd은 DNS 조회가 종료되는 시점입니다.

총 DNS 조회 시간은 종료 측정항목에서 시작 측정항목을 빼서 계산할 수 있습니다.

// Measuring DNS lookup time
const [pageNav] = performance.getEntriesByType('navigation');
const totalLookupTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;

연결 협상

로드 성능에 기여하는 또 다른 요소는 웹 서버에 연결할 때 발생하는 지연 시간인 연결 협상입니다. HTTPS가 관련된 경우 이 프로세스에 TLS 협상 시간도 포함됩니다. 연결 단계는 세 가지 타이밍으로 구성됩니다.

  • connectStart은 브라우저에서 웹 서버 연결을 열기 시작하는 시점입니다.
  • secureConnectionStart는 클라이언트가 TLS 협상을 시작하는 시점을 표시합니다.
  • connectEnd는 웹 서버에 대한 연결이 설정된 시간입니다.

총 연결 시간을 측정하는 것은 총 DNS 조회 시간을 측정하는 것과 비슷합니다. 즉, 종료 시간에서 시작 시간을 뺍니다. 하지만 HTTPS가 사용되지 않거나 연결이 영구적인 경우 0일 수 있는 추가 secureConnectionStart 속성이 있습니다. TLS 협상 시간을 측정하려면 다음 사항에 유의해야 합니다.

// Quantifying total connection time
const [pageNav] = performance.getEntriesByType('navigation');
const connectionTime = pageNav.connectEnd - pageNav.connectStart;
let tlsTime = 0; // <-- Assume 0 to start with

// Was there TLS negotiation?
if (pageNav.secureConnectionStart > 0) {
  // Awesome! Calculate it!
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

DNS 조회 및 연결 협상이 끝나면 문서 및 종속 리소스 가져오기와 관련된 타이밍이 적용됩니다.

요청 및 응답

로드 성능은 다음 두 가지 요인의 영향을 받습니다.

  • 외재적 요인: 지연 시간, 대역폭 등입니다. 호스팅 업체와 CDN을 선택하는 것 외에도 사용자는 어디서나 웹에 액세스할 수 있기 때문에 대부분 Google에서 통제할 수 없습니다.
  • 내재적 요인: 서버 및 클라이언트 측 아키텍처, 리소스 크기, Google에서 제어할 수 있는 이러한 요소에 맞게 최적화하는 기능 등이 여기에 해당합니다.

두 가지 요인 모두 로드 성능에 영향을 미칩니다. 리소스를 다운로드하는 데 걸리는 시간을 설명하므로 이러한 요소와 관련된 타이밍이 매우 중요합니다. Navigation Timing과 Resource Timing은 모두 다음 측정항목을 사용하여 로드 성능을 설명합니다.

  • fetchStart는 브라우저가 리소스 (Resource Timing) 또는 탐색 요청 문서 (Navigation Timing)를 가져오기 시작하는 시점을 나타냅니다. 실제 요청보다 선행하며 브라우저가 캐시를 확인하는 지점입니다 (예: HTTP 및 Cache 인스턴스).
  • workerStart는 서비스 워커의 fetch 이벤트 핸들러 내에서 요청이 처리되기 시작하는 시점을 표시합니다. 현재 페이지를 제어하는 서비스 워커가 없는 경우 0가 됩니다.
  • requestStart는 브라우저가 요청할 때입니다.
  • responseStart은 응답의 첫 바이트가 도착하는 시간입니다.
  • responseEnd는 응답의 마지막 바이트가 도착하는 시간입니다.

이러한 타이밍을 통해 서비스 워커 내 캐시 조회 다운로드 시간과 같은 여러 가지 로딩 성능 측면을 측정할 수 있습니다.

// Cache seek plus response time of the current document
const [pageNav] = performance.getEntriesByType('navigation');
const fetchTime = pageNav.responseEnd - pageNav.fetchStart;

// Service worker time plus response time
let workerTime = 0;

if (pageNav.workerStart > 0) {
  workerTime = pageNav.responseEnd - pageNav.workerStart;
}

요청/응답 지연 시간의 다른 측면을 측정할 수도 있습니다.

const [pageNav] = performance.getEntriesByType('navigation');

// Request time only (excluding redirects, DNS, and connection/TLS time)
const requestTime = pageNav.responseStart - pageNav.requestStart;

// Response time only (download)
const responseTime = pageNav.responseEnd - pageNav.responseStart;

// Request + response time
const requestResponseTime = pageNav.responseEnd - pageNav.requestStart;

기타 측정 방법

Navigation Timing 및 Resource Timing은 위의 예시에서 설명한 것보다 더 유용합니다. 다음과 같은 상황에서 타이밍이 꼭 필요할 수 있습니다.

  • 페이지 리디렉션: 리디렉션은 지연 시간 증가의 원인이며, 특히 리디렉션 체인에서 간과됩니다. 지연 시간은 HTTP에서 HTTPS로 가는 홉, 302/캐시되지 않은 301 리디렉션 등 다양한 방식으로 추가됩니다. redirectStart, redirectEnd, redirectCount 타이밍은 리디렉션 지연 시간을 평가하는 데 유용합니다.
  • 문서 언로드: unload 이벤트 핸들러에서 코드를 실행하는 페이지에서 브라우저는 다음 페이지로 이동하기 전에 해당 코드를 실행해야 합니다. unloadEventStartunloadEventEnd는 문서 언로드를 측정합니다.
  • 문서 처리: 웹사이트에서 매우 큰 HTML 페이로드를 전송하지 않는 한 문서 처리 시간은 중요한 영향을 미치지 않을 수 있습니다. 이 상황에 해당한다면 domInteractive, domContentLoadedEventStart, domContentLoadedEventEnd, domComplete 타이밍이 유용할 수 있습니다.

애플리케이션 코드에서 타이밍 획득

지금까지 표시된 모든 예에서는 performance.getEntriesByType를 사용하지만 performance.getEntriesByNameperformance.getEntries와 같이 성능 항목 버퍼를 쿼리하는 다른 방법도 있습니다. 이러한 방법은 광원 분석만 필요할 때 유용합니다. 하지만 다른 상황에서는 많은 항목을 반복하거나 성능 버퍼를 반복적으로 폴링하여 새 항목을 찾는 방식으로 과도한 기본 스레드 작업이 발생할 수 있습니다.

성능 항목 버퍼에서 항목을 수집하는 데 권장되는 접근 방식은 PerformanceObserver를 사용하는 것입니다. PerformanceObserver는 성능 항목을 수신 대기하고 버퍼에 추가되면 이러한 항목을 제공합니다.

// Create the performance observer:
const perfObserver = new PerformanceObserver((observedEntries) => {
  // Get all resource entries collected so far:
  const entries = observedEntries.getEntries();

  // Iterate over entries:
  for (let i = 0; i < entries.length; i++) {
    // Do the work!
  }
});

// Run the observer for Navigation Timing entries:
perfObserver.observe({
  type: 'navigation',
  buffered: true
});

// Run the observer for Resource Timing entries:
perfObserver.observe({
  type: 'resource',
  buffered: true
});

이 타이밍 수집 방법은 성능 항목 버퍼에 직접 액세스하는 것보다 어색하게 느껴질 수 있지만, 중요한 사용자 대상 목적을 제공하지 않는 작업과 기본 스레드를 연결하는 것보다 낫습니다.

Phoing Home

필요한 모든 타이밍을 수집한 후에는 추가 분석을 위해 엔드포인트로 전송할 수 있습니다. 두 가지 방법은 navigator.sendBeacon를 사용하거나 keepalive 옵션이 설정된 fetch를 사용하는 것입니다. 두 메서드 모두 비차단 방식으로 지정된 엔드포인트에 요청을 보내고 필요한 경우 요청은 현재 페이지 세션보다 오래 지속되는 방식으로 큐에 추가됩니다.

// Caution: If you have lots of performance entries, don't
// do this. This is an example for illustrative purposes.
const data = JSON.stringify(performance.getEntries()));

// The endpoint to transmit the encoded data to
const endpoint = '/analytics';

// Check for fetch keepalive support
if ('keepalive' in Request.prototype) {
  fetch(endpoint, {
    method: 'POST',
    body: data,
    keepalive: true,
    headers: {
      'Content-Type': 'application/json'
    }
  });
} else if ('sendBeacon' in navigator) {
  // Use sendBeacon as a fallback
  navigator.sendBeacon(endpoint, data);
}

이 예시에서는 JSON 문자열이 POST 페이로드에 도착하며 필요에 따라 디코딩하여 애플리케이션 백엔드에 저장할 수 있습니다.

요약

측정항목을 수집한 후 해당 필드 데이터를 분석하는 방법을 알아내는 것은 사용자의 몫입니다. 필드 데이터를 분석할 때 의미 있는 결론을 도출하기 위해 따라야 할 몇 가지 일반적인 규칙이 있습니다.

  • 평균은 피하세요. 한 사용자 경험을 대표하지 않고 이상점에 의해 왜곡될 수 있기 때문입니다.
  • 백분위수를 활용합니다. 시간 기반 성능 측정항목 데이터 세트에서는 낮을수록 좋습니다. 즉, 낮은 백분위수에 우선순위를 둘 때 가장 빠른 환경에만 주의를 기울이게 됩니다.
  • 롱테일 값의 우선순위를 지정합니다. 75번째 백분위수 이상의 환경에 우선순위를 두면 가장 느린 환경에 초점을 맞춥니다.

이 가이드는 Navigation 또는 Resource Timing에 관한 완전한 리소스가 아니라 시작에 관한 내용입니다. 다음은 도움이 될 수 있는 추가 리소스입니다.

이러한 API와 API에서 제공하는 데이터를 사용하면 실제 사용자의 로드 성능을 더 잘 이해할 수 있으므로 현장의 로드 성능 문제를 더 확실하게 진단하고 해결할 수 있습니다.