CSS @property의 성능 벤치마킹

게시: 2024년 10월 2일

새 CSS 기능을 사용하기 시작할 때는 이 기능이 웹사이트의 성능에 미치는 영향(긍정적이든 부정적이든)을 파악하는 것이 중요합니다. @property가 이제 기준점으로 설정됨에 따라 이 게시물에서는 @property의 실적 영향과 부정적인 영향을 방지하기 위해 취할 수 있는 조치를 살펴봅니다.

PerfTestRunner를 사용한 CSS 성능 벤치마킹

CSS 성능을 벤치마킹하기 위해 'CSS 선택자 벤치마크' 테스트 모음을 빌드했습니다. Chromium의 PerfTestRunner를 기반으로 하며 CSS의 성능 영향을 벤치마킹합니다. 이 PerfTestRunner는 Chromium의 기본 렌더링 엔진인 Blink가 내부 성능 테스트에 사용하는 것입니다.

실행기에는 테스트에 사용되는 measureRunsPerSecond 메서드가 포함되어 있습니다. 초당 실행 횟수가 많을수록 좋습니다. 이 라이브러리가 있는 기본 measureRunsPerSecond 벤치마크는 다음과 같습니다.

const testResults = PerfTestRunner.measureRunsPerSecond({
  "Test Description",
  iterationCount: 5,
  bootstrap: function() {
    // Code to execute before all iterations run
    // For example, you can inject a style sheet here
  },
  setup: function() {
    // Code to execute before a single iteration
  },
  run: function() {
    // The actual test that gets run and measured.
    // A typical test adjusts something on the page causing a style or layout invalidation
  },
  tearDown: function() {
    // Code to execute after a single iteration has finished
    // For example, undo DOM adjustments made within run()
  },
  done: function() {
    // Code to be run after all iterations have finished.
    // For example, remove the style sheets that were injected in the bootstrap phase
  },
});

measureRunsPerSecond의 각 옵션은 코드 블록의 주석을 통해 설명되며, run 함수가 측정되는 핵심 부분입니다.

CSS 선택자 벤치마크에는 DOM 트리가 필요합니다.

CSS 선택자의 성능도 DOM의 크기에 좌우되므로, 이러한 벤치마크에는 적절한 크기의 DOM 트리가 필요합니다. 이 DOM 트리를 수동으로 만드는 대신 이 트리가 생성됩니다.

예를 들어 다음 makeTree 함수는 @property 벤치마크의 일부입니다. 1,000개의 요소로 구성된 트리를 생성하며, 각 요소에는 일부 하위 요소가 중첩되어 있습니다.

const $container = document.querySelector('#container');

function makeTree(parentEl, numSiblings) {
  for (var i = 0; i <= numSiblings; i++) {
    $container.appendChild(
      createElement('div', {
        className: `tagDiv wrap${i}`,
        innerHTML: `<div class="tagDiv layer1" data-div="layer1">
          <div class="tagDiv layer2">
            <ul class="tagUl">
              <li class="tagLi"><b class="tagB"><a href="/" class="tagA link" data-select="link">Select</a></b></li>
            </ul>
          </div>
        </div>`,
      })
    );
  }
}

makeTree($container, 1000);

CSS 선택자 벤치마크는 DOM 트리를 수정하지 않으므로 이 트리 생성은 벤치마크가 실행되기 전에 한 번만 실행됩니다.

벤치마크 실행

테스트 모음의 일부인 벤치마크를 실행하려면 먼저 웹 서버를 시작해야 합니다.

npm run start

시작되면 게시된 URL에서 벤치마크로 이동하여 window.startTest()를 수동으로 실행할 수 있습니다.

이러한 벤치마크를 확장 프로그램이나 다른 요소의 개입 없이 개별적으로 실행하려면 PuppeteerCLI에서 트리거되어 전달된 벤치마크를 로드하고 실행합니다.

이러한 @property 벤치마크의 경우 특히 URL로 관련 페이지를 방문하는 대신 http://localhost:3000/benchmarks/at-rule/at-property.html CLI에서 다음 명령어를 호출합니다.

npm run benchmark at-rule/at-property

그러면 Puppeteer를 통해 페이지가 로드되고 window.startTest()가 자동으로 호출되어 결과를 다시 보고합니다.

CSS 속성의 성능 벤치마킹

CSS 속성의 성능을 벤치마킹하려면 스타일 무효화와 브라우저에서 실행해야 하는 후속 스타일 재계산 작업을 처리하는 속도를 벤치마킹합니다.

스타일 무효화는 DOM 변경에 대한 응답으로 스타일을 다시 계산해야 하는 요소를 표시하는 프로세스입니다. 가장 간단한 접근 방식은 모든 변경사항에 대한 응답으로 모든 항목을 무효화하는 것입니다.

이 경우 상속하는 CSS 속성과 상속하지 않는 CSS 속성 간에 차이가 있습니다.

  • 타겟팅된 요소에서 상속되는 CSS 속성이 변경되면 타겟팅된 요소 아래의 하위 트리에서 잠재적으로 모든 요소의 스타일도 변경해야 합니다.
  • 타겟팅된 요소의 변경사항을 상속하지 않는 CSS 속성이 변경되면 해당 개별 요소의 스타일만 무효화됩니다.

상속하는 속성과 상속하지 않는 속성을 비교하는 것은 공정하지 않으므로 실행할 벤치마크 세트는 두 가지입니다.

  • 상속되는 속성이 있는 벤치마크 집합입니다.
  • 상속되지 않는 속성이 있는 일련의 벤치마크입니다.

벤치마킹할 속성을 신중하게 선택하는 것이 중요합니다. 일부 속성 (예: accent-color)은 스타일만 무효화하지만 레이아웃이나 페인트와 같은 다른 항목도 무효화하는 속성 (예: writing-mode)도 많이 있습니다. 스타일만 무효화하는 속성이 필요합니다.

이를 확인하려면 Blink의 CSS 속성 목록에서 확인하세요. 각 속성에는 무효화되는 항목이 나열된 invalidate 필드가 있습니다.

또한 이러한 속성을 벤치마킹하면 결과가 왜곡되므로 목록에서 independent로 표시되지 않은 속성을 선택하는 것도 중요합니다. 독립 속성은 다른 속성이나 플래그에 부작용이 없습니다. 독립적인 속성만 변경된 경우 Blink는 하위 요소의 스타일을 클론하고 클론된 사본에서 새 값을 업데이트하는 빠른 코드 경로를 사용합니다. 이 방법은 전체 다시 계산을 수행하는 것보다 빠릅니다.

상속하는 CSS 속성의 성능 벤치마킹

첫 번째 벤치마크 세트는 상속되는 CSS 속성에 중점을 둡니다. 상속하여 서로 테스트하고 비교하는 속성에는 세 가지 유형이 있습니다.

  • accent-color를 상속하는 일반 속성입니다.
  • 등록되지 않은 맞춤 속성(--unregistered)이 있습니다.
  • inherits: true에 등록된 맞춤 속성: --registered

등록되지 않은 맞춤 속성은 기본적으로 상속되므로 이 목록에 추가됩니다.

앞서 언급한 대로 상속하는 속성은 스타일만 무효화하고 independent로 표시되지 않는 속성으로 신중하게 선택되었습니다.

등록된 맞춤 속성의 경우 inherits 설명자가 true로 설정된 속성만 이 실행에서 테스트됩니다. inherits 설명자는 속성이 하위 요소로 상속되는지 여부를 결정합니다. 등록 자체가 벤치마크의 일부가 아니므로 이 속성이 CSS @property를 통해 등록되었는지 또는 JavaScript CSS.registerProperty를 통해 등록되었는지 여부는 중요하지 않습니다.

벤치마크

이미 언급했듯이 벤치마크가 포함된 페이지는 변경사항의 영향을 확인할 수 있을 만큼 충분히 큰 노드 집합이 페이지에 포함되도록 DOM 트리를 구성하는 것으로 시작합니다.

각 벤치마크는 속성의 값을 변경한 후 스타일 무효화를 트리거합니다. 벤치마크는 기본적으로 페이지의 다음 재계산 시 무효화된 모든 스타일을 재평가하는 데 걸리는 시간을 측정합니다.

단일 벤치마크가 완료되면 다음 벤치마크를 시작할 수 있도록 삽입된 모든 스타일이 재설정됩니다.

예를 들어 --registered의 스타일 변경 성능을 측정하는 벤치마크는 다음과 같습니다.

let i = 0;
PerfTestRunner.measureRunsPerSecond({
  description,
  iterationCount: 5,
  bootstrap: () => {
    setCSS(`@property --registered {
      syntax: "<number>";
      initial-value: 0;
      inherits: true;
    }`);
  },
  setup: function() {
    // NO-OP
  },
  run: function() {
    document.documentElement.style.setProperty('--registered', i);
    window.getComputedStyle(document.documentElement).getPropertyValue('--registered'); // Force style recalculation
    i = (i == 0) ? 1 : 0;
  },
  teardown: () => {
    document.documentElement.style.removeProperty('--registered');
  },
  done: (results) => {
    resetCSS();
    resolve(results);
  },
});

다른 유형의 속성을 테스트하는 벤치마크는 동일한 방식으로 작동하지만 등록할 속성이 없으므로 빈 bootstrap가 있습니다.

결과

RAM이 16GB인 2021 MacBook Pro(Apple M1 Pro)에서 20번 반복하여 이 벤치마크를 실행하면 다음과 같은 평균값이 나옵니다.

  • 상속되는 일반 속성(accent-color): 초당 163회 실행(= 실행당 6.13ms)
  • 등록되지 않은 맞춤 속성(--unregistered): 초당 256회 실행(= 실행당 3.90ms)
  • inherits: true에 등록된 커스텀 속성 (--registered): 초당 실행 252회 (= 실행당 3.96ms)

여러 번 실행하면 벤치마크에서 비슷한 결과가 나옵니다.

상속되는 속성의 결과가 포함된 막대 그래프 숫자가 클수록 실행 속도가 빨라집니다.
그림: 상속되는 속성의 결과가 포함된 막대 그래프 숫자가 클수록 더 빠르게 실행됩니다.

결과에 따르면 맞춤 속성을 등록하는 비용은 맞춤 속성을 등록하지 않는 비용에 비해 매우 적습니다. 상속하는 등록된 맞춤 속성은 등록되지 않은 맞춤 속성의 98% 속도로 실행됩니다. 절대 수치로 보면 맞춤 속성을 등록하면 0.06ms의 오버헤드가 추가됩니다.

상속되지 않는 CSS 속성의 성능 벤치마킹

다음으로 벤치마킹할 속성은 상속되지 않는 속성입니다. 여기에서는 벤치마킹할 수 있는 속성의 두 가지 유형만 있습니다.

  • 상속되지 않는 일반 속성: z-index
  • inherits: false: --registered-no-inherit로 등록된 맞춤 속성

등록되지 않은 맞춤 속성은 항상 상속되므로 이 벤치마크에 포함될 수 없습니다.

벤치마크

벤치마크는 이전 시나리오와 매우 유사합니다. --registered-no-inherit를 사용한 테스트의 경우 다음 속성 등록이 벤치마크의 bootstrap 단계에 삽입됩니다.

@property --registered-no-inherit {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

결과

RAM이 16GB인 2021 MacBook Pro(Apple M1 Pro)에서 20번 반복하여 이 벤치마크를 실행하면 다음과 같은 평균값이 나옵니다.

  • 상속되지 않는 일반 속성: 초당 290,269회 실행 (= 실행당 3.44µs)
  • 상속되지 않는 등록된 맞춤 속성: 초당 214,110회 실행(= 실행당 4.67µs)

테스트를 여러 번 반복했으며 다음은 일반적인 결과입니다.

상속되지 않은 속성의 결과를 보여주는 막대 그래프입니다. 숫자가 클수록 속도가 더 빨라집니다.
그림: 상속되지 않는 속성의 결과가 포함된 막대 그래프 숫자가 클수록 더 빠르게 실행됩니다.

여기서 눈에 띄는 점은 상속하지 않는 속성이 상속하는 속성보다 훨씬 더 빠르게 실행된다는 것입니다. 이는 일반 속성에서 예상되는 것이지만, 맞춤 속성의 경우에도 마찬가지입니다.

  • 일반 속성의 실행 횟수는 초당 163회에서 초당 29만 회 이상으로 증가하여 성능이 1,780% 향상되었습니다.
  • 맞춤 속성의 실행 횟수는 초당 252회에서 초당 214, 000회 이상으로 증가하여 성능이 848% 향상되었습니다.

여기서 중요한 점은 맞춤 속성을 등록할 때 inherits: false를 사용하면 의미 있는 영향을 미친다는 것입니다. inherits: false로 맞춤 속성을 등록할 수 있다면 반드시 그렇게 해야 합니다.

보너스 벤치마크: 맞춤 속성 등록 여러 건

벤치마킹할 수 있는 또 다른 흥미로운 점은 맞춤 속성 등록이 많을 때의 영향입니다. 이렇게 하려면 --registered-no-inherit를 사용하여 테스트를 다시 실행하고 다른 맞춤 속성 등록 25,000개를 미리 실행합니다. 이러한 맞춤 속성은 :root에서 사용됩니다.

이러한 등록은 벤치마크의 setup 단계에서 실행됩니다.

setup: () => {
  const propertyRegistrations = [];
  const declarations = [];

  for (let i = 0; i < 25000; i++) {
    propertyRegistrations.push(`@property --custom-${i} { syntax: "<number>"; initial-value: 0; inherits: true; }`);
    declarations.push(`--custom-${i}: ${Math.random()}`);
  }

  setCSS(`${propertyRegistrations.join("\n")}
  :root {
    ${declarations.join("\n")}
  }`);
},

이 벤치마크의 초당 실행 횟수는 '상속되지 않는 등록된 맞춤 속성'의 결과와 매우 유사합니다(초당 실행 횟수 214,110회 대 초당 실행 횟수 213,158회). 하지만 여기서 주목할 부분은 이 부분이 아닙니다. 결국 하나의 맞춤 속성을 변경해도 다른 속성의 등록에 영향을 받지 않을 것으로 예상됩니다.

이 테스트에서 흥미로운 부분은 등록 자체의 영향을 측정하는 것입니다. DevTools를 보면 맞춤 속성 등록 25,000개에 초기 스타일 재계산 비용이 30ms을 약간 넘는 것으로 확인됩니다. 완료되면 이러한 등록의 존재는 더 이상 영향을 미치지 않습니다.

맞춤 속성 25,000개 등록에 대한 &#39;스타일 다시 계산&#39; 비용이 강조 표시된 DevTools 스크린샷 도움말에 32.42ms가 소요되었다고 표시됩니다.
그림: 맞춤 속성 등록 25,000회 수행에 대한 '스타일 다시 계산' 비용이 강조 표시된 DevTools 스크린샷 도움말에 걸린 시간이 32.42ms이라고 표시됩니다.

결론 및 요약

요약하면 다음 세 가지 사항을 기억해야 합니다.

  • @property에 맞춤 속성을 등록하면 약간의 성능 비용이 발생합니다. 맞춤 속성을 등록하면 속성의 잠재력을 최대한 발휘할 수 있으므로 이 비용은 무시할 수 있는 경우가 많습니다.

  • 맞춤 속성을 등록할 때 inherits: false를 사용하면 유의미한 효과를 얻을 수 있습니다. 이를 통해 속성이 상속되지 않도록 할 수 있습니다. 따라서 속성 값이 변경되면 전체 하위 트리 대신 일치하는 요소의 스타일에만 영향을 미칩니다.

  • @property 등록이 적거나 많아도 스타일 재계산에 영향을 미치지 않습니다. 등록 시 선불로 지불해야 하는 비용은 매우 적지만 등록이 완료되면 더 이상 비용이 들지 않습니다.