레이아웃 불안정성 수정

WebPageTest를 사용하여 레이아웃 불안정 문제를 식별하고 해결하는 방법을 안내합니다.

이전 게시물에서 WebPageTest의 레이아웃 변경 횟수 측정 (CLS)에 관해 설명했습니다. CLS는 모든 레이아웃 변경의 집계이므로 이 게시물에서는 불안정의 원인을 파악하고 실제로 문제를 해결하기 위해 페이지의 각 개별 레이아웃 변경을 자세히 살펴보고 검사하는 것이 흥미로울 것이라고 생각했습니다.

레이아웃 변경 측정

Layout Instability API를 사용하면 페이지의 모든 레이아웃 변경 이벤트 목록을 가져올 수 있습니다.

new Promise(resolve => {
  new PerformanceObserver(list => {
    resolve(list.getEntries().filter(entry => !entry.hadRecentInput));
  }).observe({type: "layout-shift", buffered: true});
}).then(console.log);

이렇게 하면 입력 이벤트가 앞에 나오지 않는 레이아웃 변경 배열이 생성됩니다.

[
  {
    "name": "",
    "entryType": "layout-shift",
    "startTime": 210.78500000294298,
    "duration": 0,
    "value": 0.0001045969445437389,
    "hadRecentInput": false,
    "lastInputTime": 0
  }
]

이 예에서는 210ms에서 0.01% 의 매우 작은 단일 이동이 있었습니다.

교대 근무 시간과 심각도를 알면 교대 근무의 원인을 좁히는 데 도움이 됩니다. 실험실 환경에서 더 많은 테스트를 진행하기 위해 WebPageTest로 돌아가 보겠습니다.

WebPageTest에서 레이아웃 이동 측정

WebPageTest에서 CLS를 측정하는 것과 마찬가지로 개별 레이아웃 변경을 측정하려면 맞춤 측정항목이 필요합니다. 다행히 Chrome 77이 안정화되어 이제 이 과정이 더 쉬워졌습니다. 레이아웃 불안정성 API는 기본적으로 사용 설정되어 있으므로 Chrome 77 내의 모든 웹사이트에서 해당 JS 스니펫을 실행하고 결과를 즉시 확인할 수 있습니다. WebPageTest에서는 기본 Chrome 브라우저를 사용할 수 있으며 명령줄 플래그나 Canary 사용에 대해 걱정하지 않아도 됩니다.

따라서 WebPageTest의 맞춤 측정항목을 생성하도록 스크립트를 수정해 보겠습니다.

[LayoutShifts]
return new Promise(resolve => {
  new PerformanceObserver(list => {
    resolve(JSON.stringify(list.getEntries().filter(entry => !entry.hadRecentInput)));
  }).observe({type: "layout-shift", buffered: true});
});

이 스크립트의 프로미스는 배열 자체가 아닌 배열의 JSON 표현식으로 확인됩니다. 커스텀 측정항목은 문자열이나 숫자와 같은 기본 데이터 유형만 생성할 수 있기 때문입니다.

테스트에 사용할 웹사이트는 웹 호스트의 실제 로드 성능을 비교하기 위해 제가 만든 사이트인 ismyhostfastyet.com입니다.

레이아웃 불안정의 원인 파악

결과에서 LayoutShifts 커스텀 측정항목의 값이 다음과 같음을 확인할 수 있습니다.

[
  {
    "name": "",
    "entryType": "layout-shift",
    "startTime": 3087.2349999990547,
    "duration": 0,
    "value": 0.3422101449275362,
    "hadRecentInput": false,
    "lastInputTime": 0
  }
]

요약하자면 3087ms에 34.2% 의 단일 레이아웃 변경이 발생합니다. 범인을 식별하기 위해 WebPageTest의 필름 스트립 보기를 사용해 보겠습니다.

필름스트립의 두 셀에 레이아웃 변경 전후의 스크린샷이 표시됩니다.
레이아웃 변경 전후의 스크린샷을 보여주는 필름스트립의 두 셀

필름스트립에서 약 3초 지점까지 스크롤하면 34% 레이아웃 이동의 원인이 다채로운 표임을 정확히 알 수 있습니다. 웹사이트는 JSON 파일을 비동기적으로 가져온 다음 테이블에 렌더링합니다. 표가 처음에는 비어 있으므로 결과가 로드될 때까지 기다려 채우면 이동이 발생합니다.

웹 글꼴 헤더가 갑자기 표시됨
웹 글꼴 헤더가 갑자기 표시됩니다.

그뿐만이 아닙니다. 페이지가 시각적으로 완료되는 시점인 약 4.3초에 페이지의 <h1>인 '내 호스트는 아직 빠르지 않나요?'가 갑자기 표시됩니다. 사이트에서 웹 글꼴을 사용하고 렌더링을 최적화하기 위한 단계를 취하지 않았기 때문에 이러한 문제가 발생합니다. 이 경우 레이아웃이 실제로 이동하는 것처럼 보이지는 않지만 제목을 읽기 위해 이렇게 오래 기다려야 하는 것은 좋지 않은 사용자 환경입니다.

레이아웃 불안정성 수정

비동기적으로 생성된 표로 인해 뷰포트의 1/3이 이동하는 것을 확인했으므로 이제 이 문제를 해결할 차례입니다. JSON 결과가 실제로 로드될 때까지는 표의 내용을 알 수 없지만 DOM이 렌더링될 때 레이아웃 자체가 비교적 안정적이도록 자리표시자 데이터를 사용하여 표를 채울 수는 있습니다.

다음은 자리표시자 데이터를 생성하는 코드입니다.

function getRandomFiller(maxLength) {
  var filler = '█';
  var len = Math.ceil(Math.random() * maxLength);
  return new Array(len).fill(filler).join('');
}

function getRandomDistribution() {
  var fast = Math.random();
  var avg = (1 - fast) * Math.random();
  var slow = 1 - (fast + avg);
  return [fast, avg, slow];
}

// Temporary placeholder data.
window.data = [];
for (var i = 0; i < 36; i++) {
  var [fast, avg, slow] = getRandomDistribution();
  window.data.push({
    platform: getRandomFiller(10),
    client: getRandomFiller(5),
    n: getRandomFiller(1),
    fast,
    avg,
    slow
  });
}
updateResultsTable(sortResults(window.data, 'fast'));

자리표시자 데이터는 정렬되기 전에 무작위로 생성됩니다. 텍스트의 시각적 자리표시자를 만들기 위해 임의의 횟수로 반복되는 '█' 문자와 세 가지 주요 값의 무작위로 생성된 분포가 포함됩니다. 데이터가 아직 완전히 로드되지 않았음을 명확하게 나타내기 위해 테이블의 모든 색상을 채도 감소시키는 스타일도 추가했습니다.

사용하는 자리표시자의 모양은 레이아웃 안정성에 영향을 미치지 않습니다. 자리표시자의 목적은 콘텐츠가 곧 제공될 것이며 페이지가 깨지지 않았음을 사용자에게 알리는 것입니다.

JSON 데이터를 로드하는 동안 자리표시자는 다음과 같이 표시됩니다.

데이터 표가 자리표시자 데이터로 렌더링됩니다.
데이터 테이블이 자리표시자 데이터로 렌더링됩니다.

웹 글꼴 문제를 해결하는 것이 훨씬 간단합니다. 사이트에서 Google Fonts를 사용하므로 CSS 요청에서 display=swap 속성만 전달하면 됩니다. 여기까지입니다. Fonts API는 글꼴 선언에 font-display: swap 스타일을 추가하여 브라우저가 대체 글꼴로 텍스트를 즉시 렌더링할 수 있도록 합니다. 수정사항이 포함된 해당 마크업은 다음과 같습니다.

<link href="https://fonts.googleapis.com/css?family=Chivo:900&display=swap" rel="stylesheet">

최적화 확인

WebPageTest를 통해 페이지를 다시 실행한 후에는 전후 비교를 생성하여 차이를 시각화하고 새로운 레이아웃 불안정성 정도를 측정할 수 있습니다.

레이아웃 최적화가 적용된 사이트와 적용되지 않은 사이트가 나란히 로드되는 WebPageTest 필름 스트립
레이아웃 최적화가 적용된 사이트와 적용되지 않은 사이트가 나란히 로드되는 WebPageTest 필름 스트립
[
  {
    "name": "",
    "entryType": "layout-shift",
    "startTime": 3070.9349999997357,
    "duration": 0,
    "value": 0.000050272187989256116,
    "hadRecentInput": false,
    "lastInputTime": 0
  }
]

맞춤 측정항목에 따르면 3071ms(이전과 거의 동일한 시간)에서 레이아웃 이동이 발생하지만 이동의 심각도는 훨씬 작습니다(0.005%). 이 정도면 괜찮습니다.

또한 필름 스트립에서 <h1> 글꼴이 즉시 시스템 글꼴로 대체되어 사용자가 더 빨리 읽을 수 있음을 알 수 있습니다.

결론

복잡한 웹사이트는 이 예보다 훨씬 더 많은 레이아웃 이동을 경험할 수 있지만 수정 프로세스는 동일합니다. WebPageTest에 레이아웃 불안정성 측정항목을 추가하고, 시각적 로드 필름 스트립과 결과를 상호 참조하여 원인을 파악하고, 자리표시자를 사용하여 화면 공간을 예약하여 수정사항을 구현합니다.

(한 가지 더) 실제 사용자가 경험하는 레이아웃 불안정성 측정

최적화 전후에 페이지에서 WebPageTest를 실행하고 측정항목이 개선되는 것을 확인하는 것도 좋지만, 실제로 사용자 환경이 개선되는 것이 중요합니다. 사이트를 개선하려고 하는 이유가 바로 그 때문 아닌가요?

따라서 기존 웹 성능 측정항목과 함께 실제 사용자의 레이아웃 불안정성 경험을 측정하는 것이 좋습니다. 이는 최적화 피드백 루프의 중요한 부분입니다. 필드의 데이터를 통해 문제가 있는 위치와 수정사항이 긍정적인 영향을 미쳤는지 알 수 있기 때문입니다.

자체 레이아웃 불안정성 데이터를 수집하는 것 외에도 수백만 개의 웹사이트에서 실제 사용자 환경의 누적 레이아웃 이동 데이터를 포함하는 Chrome UX 보고서를 확인하세요. 이를 통해 나와 경쟁업체의 실적을 파악하거나 웹 전반의 레이아웃 불안정 상태를 살펴볼 수 있습니다.