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

레이아웃은 브라우저가 요소의 기하학적 정보(페이지에서의 크기 및 위치)를 파악하는 장소입니다. 각 요소는 사용된 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를 사용하면 브라우저가 레이아웃을 더 일찍 수행하도록 할 수 있습니다. 이를 강제 동기식 레이아웃이라고 합니다.

가장 먼저 명심해야 할 점은 자바스크립트가 실행될 때 이전 프레임의 모든 이전 레이아웃 값이 알려져 있고 사용자가 쿼리할 수 있다는 것입니다. 예를 들어 프레임 시작 부분에 요소의 높이('상자'라고 함)를 쓰려면 다음과 같은 코드를 작성할 수 있습니다.

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

Hal GatewoodUnsplash의 히어로 이미지입니다.