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

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

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

스타일 계산과 마찬가지로 레이아웃 비용과 관련하여 즉시 고려해야 할 사항은 다음과 같습니다.

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

요약

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

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

사용자가 페이지와 상호작용할 때 이러한 상호작용은 최대한 빨라야 합니다. 상호작용이 완료되는 데 걸리는 시간(브라우저가 다음 프레임을 표시하여 상호작용 결과를 표시할 때 종료됨)을 상호작용 지연 시간이라고 합니다. 이는 다음 페인트에 대한 상호작용 측정항목에서 측정하는 페이지 성능의 한 측면입니다.

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

웹사이트의 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를 실행한 다음 스타일을 계산한 다음 레이아웃을 실행합니다. 하지만 자바스크립트를 사용하여 브라우저가 레이아웃을 더 일찍 실행하도록 강제할 수도 있습니다. 이를 강제 동기식 레이아웃이라고 합니다.

자바스크립트가 실행할 때 이전 프레임의 모든 이전 레이아웃 값은 알려져 있고 쿼리에 사용할 수 있습니다. 예를 들어, 프레임 시작 부분에 요소('상자'라고 함)의 높이를 기록하려는 경우 다음과 같은 코드를 작성할 수 있습니다.

// 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을 사용하는 것이 좋습니다. 그러면 실수로 강제 동기식 레이아웃 또는 레이아웃 스래싱을 트리거하지 않도록 할 수 있습니다.