스타일 계산의 범위와 복잡성 줄이기

JavaScript는 종종 시각적 변화를 촉발합니다. 어떤 경우에는 스타일 조작을 통해 직접 구현되거나 어떤 경우에는 데이터 검색 또는 정렬과 같은 시각적 변화를 일으키는 계산을 통해 구현됩니다. 타이밍이 나쁘거나 실행 시간이 긴 JavaScript는 성능 문제의 일반적인 원인이 될 수 있으며, 가급적 이러한 영향을 최소화할 수 있는 방법을 찾아야 합니다.

DOM 변경, 요소 추가 및 제거, 속성 변경, 클래스 또는 애니메이션을 통해 모두 브라우저가 요소 스타일을 다시 계산하게 되고 대부분의 경우 페이지 또는 페이지의 일부를 레이아웃 (또는 리플로우)하게 됩니다. 이 프로세스를 계산된 스타일 계산이라고 합니다.

컴퓨팅 스타일의 첫 번째 부분은 매칭 선택기 집합을 만드는 것입니다. 이는 기본적으로 브라우저가 주어진 요소에 적용할 클래스, 의사 선택기 및 ID를 파악하는 것입니다.

프로세스의 두 번째 단계에는 매칭 선택기에서 모든 스타일 규칙을 가져와서 요소의 최종 스타일을 파악해야 합니다.

요약

  • 스타일 계산 비용을 절감하여 상호작용 지연 시간을 줄이는 방법
  • 선택기의 복잡성을 줄이고 클래스 중심 방법론 (예: BEM)을 사용합니다.
  • 스타일 계산을 계산해야 하는 요소 수를 줄입니다.

스타일 재계산 시간 및 상호작용 지연 시간

다음 페인트에 대한 상호작용 (INP)은 사용자 입력에 대한 페이지의 전반적인 응답성을 평가하는 사용자 중심의 런타임 성능 측정항목입니다. 이 측정항목으로 상호작용 지연 시간을 평가하면 사용자가 페이지와 상호작용한 시점부터 브라우저가 다음 프레임을 그려 사용자 인터페이스의 해당 시각적 업데이트를 표시할 때까지의 시간을 측정합니다.

상호작용의 중요한 구성요소는 다음 프레임을 그리는 데 걸리는 시간입니다. 다음 프레임을 표시하기 위해 수행되는 렌더링 작업은 레이아웃, 페인트 및 합성 작업 직전에 발생하는 페이지 스타일 계산을 비롯한 많은 부분으로 구성됩니다. 이 문서에서는 스타일 계산 비용에만 중점을 두지만, 상호작용에 고유한 렌더링 단계를 줄이면 총 지연 시간이 줄어들고 스타일 계산을 포함한다는 점을 강조해야 합니다.

선택기의 복잡성 줄이기

가장 간단한 경우 클래스만 있는 CSS의 요소를 참조할 수 있습니다.

.title {
  /* styles */
}

하지만 프로젝트가 성장함에 따라 CSS가 더 복잡해지고 다음과 같은 선택기가 생성될 수 있습니다.

.box:nth-last-child(-n+1) .title {
  /* styles */
}

이러한 스타일이 페이지에 어떻게 적용되는지 파악하기 위해 브라우저는 "이것이 상위 요소에서 n번째 하위 요소 + box 클래스를 가진 1개의 요소가 되는 title 클래스가 있는 요소인가요?"라는 질문을 효과적으로 해야 합니다. 이를 파악하는 데는 사용하는 선택기와 문제가 되는 브라우저에 따라 시간이 오래 걸릴 수도 있습니다. 대신 선택기의 의도된 동작을 클래스로 변경할 수 있습니다.

.final-box-title {
  /* styles */
}

클래스 이름에 문제가 있을 수 있지만 브라우저에서 작업이 훨씬 간단해졌습니다. 예를 들어 이전 버전에서는 요소가 유형의 마지막임을 알 수 있도록 브라우저가 먼저 다른 모든 요소에 관한 모든 정보를 파악하고 그 뒤에 오는 요소가 nth-last-child인지를 알아야 합니다. 이는 클래스가 일치하므로 단순히 선택기를 요소에 매칭하는 것보다 비용이 더 많이 들 수 있습니다.

스타일 지정 요소 수 줄이기

또 다른 성능 고려사항(일반적으로 많은 스타일 업데이트에서 더 중요한 요소)은 요소 변경 시 수행해야 하는 순수한 작업량입니다.

일반적으로 요소의 계산된 스타일을 계산하는 최악의 경우 비용은 요소 수에 선택기 수를 곱한 것입니다. 각 요소가 일치하는지 확인하려면 모든 스타일에 대해 최소 한 번 확인해야 하기 때문입니다.

스타일 계산은 페이지 전체를 무효화하는 대신 몇 가지 요소를 직접 대상으로 하는 경우가 많습니다. 최신 브라우저에서는 브라우저가 변경사항의 영향을 받았을 가능성이 있는 모든 요소를 확인할 필요가 없기 때문에 문제 발생 가능성이 적습니다. 반면에 이전 브라우저는 이러한 작업에 맞게 최적화되지 않았을 수 있습니다. 가능하면 무효화되는 요소 수를 줄여야 합니다.

스타일 재계산 비용 측정

스타일 재계산 비용을 측정하는 한 가지 방법은 Chrome DevTools의 성능 패널을 사용하는 것입니다. 시작하려면 DevTools를 열고 성능 탭으로 이동하여 녹화를 누르고 페이지와 상호작용합니다. 녹화를 중지하면 아래와 같은 이미지가 표시됩니다.

스타일 계산을 보여주는 DevTools

상단의 스트립은 초당 프레임 수를 표시하는 소형 플레임 차트입니다. 활동이 스트립 하단에 가까울수록 브라우저가 더 빠르게 프레임을 그립니다. 상단에 빨간색 스트립이 있는 상단이 수평으로 돌아가고 있는 Flame Chart가 표시되면 장기 실행 프레임의 원인이 되는 작업이 있는 것입니다.

Chrome DevTools의 채워진 성능 패널의 활동 요약에서 Chrome DevTools의 문제 영역을 확대한 모습.

스크롤과 같은 상호작용 중에 오래 실행되는 프레임이 있다면 좀 더 꼼꼼하게 검토해야 합니다. 큰 보라색 블록이 있는 경우 활동을 확대하고 Recalculate Style이라는 라벨이 지정된 작업을 선택하여 비용이 많이 들 수 있는 스타일 재계산 작업에 관한 자세한 정보를 확인합니다.

스타일 재계산 작업의 영향을 받는 요소의 양과 같은 중요한 정보를 비롯하여 오랫동안 실행되는 스타일 계산의 세부정보를 가져옵니다.

이 그래프에는 25ms가 조금 넘게 걸리는 장기 실행 스타일 재계산 작업이 있습니다.

이벤트 자체를 클릭하면 호출 스택이 제공됩니다. 렌더링 작업이 사용자 상호작용으로 인해 발생한 경우 JavaScript에서 스타일 변경을 트리거하는 위치가 호출됩니다. 또한 변경사항의 영향을 받은 요소의 수(이 경우에는 900개 미만)와 스타일 계산 작업을 수행하는 데 걸린 시간도 확인할 수 있습니다. 이 정보를 사용하여 코드에서 수정 사항을 찾을 수 있습니다.

블록, 요소, 특수키 사용

BEM (블록, 요소, 수정자)과 같은 코딩 접근 방식은 위의 선택기 매칭 성능 이점을 기본적으로 구현합니다. 모든 것에 단일 클래스가 있고 계층 구조가 필요한 경우 클래스의 이름에도 결합되는 것이 좋기 때문입니다.

.list {
  /* Styles */
}

.list__list-item {
  /* Styles */
}

위와 같이 마지막 하위 요소에 대해 특별한 작업을 하고 싶은 곳에 수정자가 필요하면 다음과 같이 수정자를 추가하면 됩니다.

.list__list-item--last-child {
  /* Styles */
}

CSS를 구성하는 좋은 방법을 찾고 있다면 BEM이 좋은 출발점이 될 수 있습니다. 구조적 관점에서 그리고 방법론에서 권장하는 스타일 조회의 단순화 때문이기도 합니다.

BEM이 마음에 들지 않는다면 CSS를 이용하는 다른 방법도 있지만, 인체공학적 접근 방식과 함께 성능 고려사항을 고려해야 합니다.

자료

Unsplash의 히어로 이미지(Markus Spiske 제작)