크고 복잡한 레이아웃 및 레이아웃 스래싱 피하기

레이아웃은 브라우저가 요소의 기하학적 정보(페이지에서 차지하는 크기 및 위치)를 파악하는 장소입니다. 각 요소는 사용된 CSS, 요소의 콘텐츠 또는 상위 요소에 따라 명시적 또는 암시적 크기 지정 정보를 갖게 됩니다. 이 프로세스는 Chrome에서 레이아웃이라고 합니다.

레이아웃은 브라우저가 요소의 기하학적 정보(페이지에서 차지하는 크기 및 위치)를 파악하는 장소입니다. 각 요소는 사용된 CSS, 요소의 콘텐츠 또는 상위 요소에 따라 명시적 또는 암시적 크기 지정 정보를 갖게 됩니다. 이 프로세스는 Chrome (및 Edge와 같은 파생 브라우저) 및 Safari에서 레이아웃이라고 합니다. Firefox에서는 리플로우(reflow)라고 하지만 프로세스는 사실상 동일합니다.

스타일 계산과 마찬가지로 레이아웃 비용에 대한 즉각적인 문제는 다음과 같습니다.

  1. 레이아웃이 필요한 요소 수(페이지의 DOM 크기의 부산물)
  2. 이러한 레이아웃의 복잡성

요약

  • 레이아웃은 상호작용 지연 시간에 직접적인 영향을 미칩니다.
  • 레이아웃의 범위는 일반적으로 전체 문서로 지정됩니다.
  • DOM 요소 수는 성능에 영향을 주므로 가급적 레이아웃 트리거를 피해야 합니다.
  • 강제 동기식 레이아웃 및 레이아웃 스래싱을 피하세요. 스타일 값을 읽은 다음 스타일을 변경하세요.

상호작용이 지연 시간에 미치는 레이아웃의 영향

사용자가 페이지와 상호작용할 때 이러한 상호작용은 가능한 한 빠르게 이루어져야 합니다. 상호작용이 완료되는 데 걸리는 시간(브라우저가 상호작용 결과를 표시하기 위해 다음 프레임을 표시하는 시점까지)을 상호작용 지연 시간이라고 합니다. 이는 Interaction to Next Paint(다음 페인트에 대한 상호작용) 측정항목이 측정하는 페이지 성능의 한 측면입니다.

사용자 상호작용에 응답하여 브라우저에서 다음 프레임을 표시하는 데 걸리는 시간을 상호작용의 프레젠테이션 지연이라고 합니다. 상호작용의 목표는 사용자에게 어떤 일이 발생했다는 신호를 보내기 위해 시각적 피드백을 제공하는 것이며, 시각적 업데이트에는 이러한 목표를 달성하기 위해 어느 정도의 레이아웃 작업이 포함될 수 있습니다.

웹사이트의 INP를 가능한 한 낮게 유지하려면 레이아웃을 최대한 피하는 것이 중요합니다. 레이아웃을 완전히 피할 수 없다면 브라우저가 다음 프레임을 빠르게 표시할 수 있도록 해당 레이아웃 작업을 제한하는 것이 중요합니다.

가능하면 레이아웃 피하기

스타일을 변경하면 브라우저가 변경 사항에 레이아웃 계산이 필요한지, 해당 렌더링 트리를 업데이트해야 하는지 확인합니다. 너비, 높이, 왼쪽 또는 상단과 같은 '기하학적 속성'의 변경에는 모두 레이아웃이 필요합니다.

.box {
  width: 20px;
  height: 20px;
}

/**
  * Changing width and height
  * triggers layout.
  */

.box--expanded {
  width: 200px;
  height: 350px;
}

레이아웃의 범위는 거의 항상 전체 문서로 지정됩니다. 요소가 많은 경우 모든 요소의 위치와 크기를 파악하는 데 시간이 오래 걸립니다.

레이아웃을 피할 수 없다면 다시 한 번 Chrome DevTools를 사용하여 시간이 얼마나 걸리는지 확인하고 레이아웃으로 인해 병목 현상이 발생하는지 확인하는 것이 중요합니다. 먼저 DevTools를 열고 Timeline 탭으로 이동하여 '기록'을 누르고 사이트와 상호작용합니다. 레코딩을 중지하면 사이트 성능에 대한 분석 결과가 표시됩니다.

레이아웃에서 오랜 시간을 표시하는 DevTools

위 예에서 트레이스를 자세히 살펴보면 각 프레임에 대해 레이아웃 내에서 28밀리초 이상을 소요한다는 것을 알 수 있습니다. 이는 애니메이션에서 화면에 프레임을 가져오는 데 16밀리초가 소요되는 경우 매우 높습니다. 또한 DevTools에서 트리 크기 (여기서는 1,618개 요소)와 레이아웃이 필요한 노드 수 (여기서는 5)를 알려주는 것도 확인할 수 있습니다.

여기서 일반적인 권장사항은 가능한 한 레이아웃을 피하는 것이지만 항상 레이아웃을 피할 수 있는 것은 아닙니다. 레이아웃을 피할 수 없는 경우에는 레이아웃 비용이 DOM 크기와 관계가 있다는 점을 알아두세요. 둘 사이의 관계가 밀접하게 연결되어 있지는 않지만 일반적으로 DOM이 크면 레이아웃 비용이 높아집니다.

강제 동기식 레이아웃 피하기

화면에 프레임을 배송하는 순서는 다음과 같습니다.

Flexbox를 레이아웃으로 사용 중입니다.

먼저 JavaScript를 실행한 다음 스타일을 계산한 다음 레이아웃을 실행합니다. 그러나 JavaScript를 사용하여 브라우저가 레이아웃을 더 일찍 수행하도록 할 수 있습니다. 이를 강제 동기식 레이아웃이라고 합니다.

가장 먼저 명심해야 할 점은 JavaScript가 실행되기 때문에 이전 프레임의 모든 이전 레이아웃 값이 알려져 있으며 쿼리에 사용할 수 있다는 점입니다. 예를 들어, 프레임 시작 부분에 요소('상자'라고 함)의 높이를 기록하려는 경우 다음과 같은 코드를 작성할 수 있습니다.

// Schedule our function to run at the start of the frame:
requestAnimationFrame(logBoxHeight);

function logBoxHeight () {
  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);
}

높이를 요청하기 전에 상자의 스타일을 변경하면 문제가 발생합니다.

function logBoxHeight () {
  box.classList.add('super-big');

  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);
}

이제 높이 질문에 답하려면 브라우저에서 먼저 스타일 변경사항을 적용한 후 (super-big 클래스를 추가했기 때문에) 그런 다음 레이아웃을 실행해야 합니다. 그래야만 올바른 높이를 반환할 수 있습니다. 이는 불필요하고 잠재적으로 비용이 많이 드는 작업입니다.

따라서 항상 스타일 읽기를 일괄 처리하고 먼저 수행한 다음 (브라우저가 이전 프레임의 레이아웃 값을 사용할 수 있음) 쓰기를 수행해야 합니다.

위의 함수가 올바르게 완료되면 다음과 같습니다.

function logBoxHeight () {
  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

대부분의 경우 스타일을 적용한 다음 값을 쿼리할 필요가 없습니다. 마지막 프레임의 값을 사용하는 것으로 충분합니다. 브라우저가 원하는 것보다 일찍, 동기식으로 스타일 계산과 레이아웃을 실행하면 병목 현상이 발생할 수 있으므로 일반적으로는 바람직하지 않습니다.

레이아웃 스래싱 피하기

많은 레이아웃을 연속적으로 빠르게 실행하여 강제 동기식 레이아웃을 더 악화시키는 방법이 있습니다. 다음 코드를 살펴보세요.

function resizeAllParagraphsToMatchBlockWidth () {
  // Puts the browser into a read-write-read-write cycle.
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = `${box.offsetWidth}px`;
  }
}

이 코드는 단락 그룹을 반복하고 각 단락의 너비를 'box'라는 요소의 너비와 일치하도록 설정합니다. 무해해 보이지만, 문제는 루프의 각 반복이 스타일 값 (box.offsetWidth)을 읽은 다음 즉시 이를 사용하여 단락의 너비 (paragraphs[i].style.width)의 너비를 업데이트한다는 것입니다. 다음 루프 반복에서 브라우저는 offsetWidth가 마지막으로 요청된 이후 (이전 반복에서) 스타일이 변경되었다는 사실을 고려해야 하므로 스타일 변경사항을 적용하고 레이아웃을 실행해야 합니다. 이는 모든 반복에서 발생합니다.

이 샘플을 수정하려면 값을 다시 읽은쓰는 것입니다.

// Read.
const width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth () {
  for (let i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = `${width}px`;
  }
}

안전을 보장하려면 읽기 및 쓰기를 자동으로 일괄 처리하며 실수로 강제 동기식 레이아웃 또는 레이아웃 스래싱을 트리거하지 않도록 하는 FastDOM을 사용하는 것이 좋습니다.

할 게이트우드Unsplash의 히어로 이미지