레이아웃 불안정성 수정

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이 안정화되어 프로세스가 더 쉬워졌습니다. Layout Instability 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>가 갑자기 표시됩니다. 이는 사이트에서 웹 글꼴을 사용하고 렌더링을 최적화하기 위한 조치를 취하지 않았기 때문에 발생합니다. 이 경우 레이아웃이 실제로 이동하는 것처럼 보이지는 않지만 제목을 읽기 위해 그렇게 오래 기다려야 하는 것은 좋지 않은 사용자 환경입니다.

레이아웃 불안정성 수정

이제 비동기식으로 생성된 테이블로 인해 뷰포트의 3분의 1이 이동한다는 것을 알았으므로 이를 수정해야 합니다. 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
  }
]

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

필름 스트립에서 <h1> 글꼴이 즉시 시스템 글꼴로 대체되어 사용자가 더 빨리 읽을 수 있다는 점도 분명합니다.

결론

복잡한 웹사이트에서는 이 예시보다 더 많은 레이아웃 변경을 경험할 수 있지만 해결 프로세스는 여전히 동일합니다. 레이아웃 불안정성 측정항목을 WebPageTest에 추가하고, 시각적 로딩 슬라이드와 결과를 교차 참조하여 원인을 식별하고, 자리표시자를 사용하여 수정 사항을 구현하여 화면 공간을 예약합니다.

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

최적화 전후에 페이지에서 WebPageTest를 실행하고 측정항목이 개선되는 것을 확인하는 것도 좋지만, 실제로 중요한 것은 사용자 환경이 실제로 개선되는 것입니다. 혹시 애초에 사이트를 개선하려고 노력하는 이유가 아니죠?

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

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