Porównywanie skuteczności właściwości @property w usłudze porównywania cen

Data publikacji: 2 października 2024 r.

Gdy zaczniesz korzystać z nowej funkcji CSS, warto poznać jej wpływ na skuteczność Twoich witryn, zarówno pozytywny, jak i negatywny. Funkcja @property jest teraz dostępna w wersji podstawowej, więc w tym poście omawiamy jej wpływ na skuteczność reklam i sposoby zapobiegania negatywnym skutkom.

Porównywanie wydajności usługi porównywania cen z PerfTestRunner

Aby porównać skuteczność CSS, stworzyliśmy pakiet testowy „CSS Selector Benchmark”. Jest ona obsługiwana przez Chromium PerfTestRunner i mierzy wpływ CSS na wydajność. Ten komponent PerfTestRunner używa bazowego silnika renderowania Blink–Chromium w wewnętrznych testach wydajności.

Runner zawiera metodę measureRunsPerSecond, która jest używana do testów. Im większa liczba przebiegów na sekundę, tym lepiej. Podstawowy test porównawczy measureRunsPerSecond z tą biblioteką wygląda tak:

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
  },
});

Każda opcja funkcji measureRunsPerSecond jest opisana w komentarzach w bloku kodu, a funkcja run jest główną częścią, która jest mierzona.

Testy porównawcze selektora CSS wymagają drzewa DOM

Wydajność selektorów CSS zależy też od rozmiaru interfejsu DOM, więc te punkty odniesienia wymagają odpowiednio dużego drzewa DOM. Zamiast tworzyć drzewo DOM ręcznie, możesz wygenerować je automatycznie.

Na przykład funkcja makeTree jest częścią testów porównawczych @property. Tworzy drzewo 1000 elementów, z których każdy zawiera zagnieżdżone elementy podrzędne.

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);

Ponieważ punkty odniesienia selektora CSS nie modyfikują drzewa DOM, generowanie tego drzewa jest wykonywane tylko raz, przed uruchomieniem dowolnego punktu odniesienia.

Przeprowadzanie testu porównawczego

Aby uruchomić test porównawczy, który jest częścią zestawu testów, musisz najpierw uruchomić serwer WWW:

npm run start

Po uruchomieniu testu możesz przejść do testu porównawczego pod opublikowanym adresem URL i ręcznie wykonać polecenie window.startTest().

Aby uruchomić te testy porównawcze oddzielnie – bez żadnych rozszerzeń ani innych czynników – Puppeteer jest uruchamiany z poziomu wiersza poleceń w celu załadowania i wykonania przekazanego testu porównawczego.

W przypadku tych @propertybenchmarków zamiast odwiedzać odpowiednią stronę pod adresem URL http://localhost:3000/benchmarks/at-rule/at-property.html, uruchom w interfejsie wiersza poleceń te polecenia:

npm run benchmark at-rule/at-property

Spowoduje to załadowanie strony przez Puppeteer, automatycznie wywołuje metodę window.startTest() i zwraca wyniki.

Porównywanie skuteczności właściwości usługi porównywania cen

Aby przetestować wydajność właściwości CSS, sprawdź, jak szybko może ona obsłużyć unieważnienie stylu i kolejną operację ponownego obliczania stylu, którą musi wykonać przeglądarka.

Unieważnienie stylu to proces oznaczania elementów, które wymagają przeliczenia stylu w odpowiedzi na zmianę w DOM. Najprostszym sposobem jest unieważnienie wszystkiego w odpowiedzi na każdą zmianę.

W tym przypadku należy odróżnić właściwości CSS, które dziedziczą, od tych, które tego nie robią.

  • Gdy w elementach docelowych zmieni się właściwość CSS, która dziedziczy, należy zmienić style wszystkich elementów w poddrzewie podrzędnym pod element docelowy.
  • Gdy właściwość CSS, która nie dziedziczy zmian w docelowym elemencie, traci ważność, nieważne stają się tylko style tego elementu.

Porównywanie właściwości, które dziedziczą, z tymi, które tego nie robią, nie byłoby sprawiedliwe, dlatego należy uruchomić 2 zestawy punktów odniesienia:

  • Zestaw punktów odniesienia z właściwościami, które je dziedziczą.
  • Zestaw punktów odniesienia z właściwościami, które nie są dziedziczone.

Należy starannie wybrać usługi, które mają posłużyć jako punkt odniesienia. Niektóre właściwości (np. accent-color) unieważniają tylko style, ale wiele właściwości (np. writing-mode) unieważnia także inne elementy, takie jak układ czy wyrenderowanie. Potrzebujesz właściwości, które unieważniają tylko style.

Aby to ustalić, sprawdź listę właściwości CSS w Blinku. Każda usługa ma pole invalidate, które zawiera informacje o tym, co zostaje unieważnione.

Ponadto z tej listy wybierz usługę, która nie jest oznaczona jako independent, ponieważ porównanie z taką usługą może zafałszować wyniki. Niezależne właściwości nie mają wpływu na inne właściwości ani flagi. Gdy zmieniły się tylko właściwości niezależne, Blink używa szybkiej ścieżki kodu, która klonuje styl potomka i aktualizuje nowe wartości w sklonowanej kopii. Jest to szybsze niż pełne ponowne obliczanie.

Porównywanie skuteczności właściwości CSS, które dziedziczą

Pierwszy zestaw punktów odniesienia koncentruje się na właściwościach CSS, które dziedziczą. Istnieją 3 rodzaje właściwości, które dziedziczą testy i porównują je ze sobą:

  • Zwykła właściwość, która dziedziczy właściwość accent-color.
  • Niezarejestrowana właściwość niestandardowa: --unregistered.
  • Właściwość niestandardowa zarejestrowana w inherits: true: --registered.

Niezarejestrowane właściwości niestandardowe są dodawane do tej listy, ponieważ są one domyślnie dziedziczone.

Jak wspomnieliśmy wcześniej, właściwość dziedziczona została starannie wybrana, aby unieważniać tylko style, a druga nieoznaczona jako independent.

W przypadku zarejestrowanych usług niestandardowych w tym przebiegu testowane są tylko te, w których przypadku parametr inherits ma wartość true. Deskryptor inherits określa, czy właściwość jest dziedziczona przez elementy podrzędne. Nie ma znaczenia, czy ta usługa jest zarejestrowana za pomocą CSS @property czy JavaScript CSS.registerProperty, ponieważ sama rejestracja nie jest uwzględniana w benchmarku.

Testy porównawcze

Jak już wspomnieliśmy, strona z benchmarkami zaczyna się od tworzenia drzewa DOM, aby strona miała wystarczająco duży zbiór węzłów, który umożliwi obserwowanie wpływu zmian.

Każdy punkt odniesienia zmienia wartość właściwości, po czym powoduje unieważnienie stylu. Test porównawczy to przede wszystkim czas potrzebny na kolejne przeliczenie strony, aby przeprowadzić ponowną ocenę wszystkich unieważnionych stylów.

Po zakończeniu pojedynczego testu porównawczego wszystkie wstrzyknięte style są resetowane, aby można było rozpocząć kolejny test porównawczy.

Na przykład test porównawczy dotyczący skuteczności zmiany stylu elementu --registered wygląda tak:

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);
  },
});

Wzorce testujące inne typy usług działają tak samo, ale mają pustą wartość bootstrap, ponieważ nie ma usługi do zarejestrowania.

Wyniki

Uruchomienie tych testów z 20 powtórzeniami na MacBooku Pro (Apple M1 Pro) z 2021 roku z 16 GB pamięci RAM daje te średnie wartości:

  • Zwykła usługa, która dziedziczy (accent-color): 163 wykonania na sekundę (= 6,13 ms na wykonanie).
  • Niezarejestrowana właściwość niestandardowa (--unregistered): 256 wywołań na sekundę (= 3,90 ms na wywołanie).
  • Zarejestrowana usługa niestandardowa z inherits: true (--registered): 252 wykonania na sekundę (czyli 3,96 ms na wykonanie).

W przypadku wielu uruchomień testy porównawcze dają podobne wyniki.

Wykres słupkowy z wynikami dotyczącymi właściwości, które dziedziczą. Im wyższa wartość, tym szybsze działanie.
Ilustracja: wykres słupkowy z wynikami dotyczącymi właściwości, które dziedziczą. Im wyższa wartość, tym szybsze działanie.

Wyniki pokazują, że rejestracja usługi niestandardowej wiąże się z bardzo niewielkimi kosztami w porównaniu z niezarejestrowaniem usługi niestandardowej. Zarejestrowane usługi niestandardowe, które dziedziczą ustawienia, działają z prędkością 98% prędkości nierejestrowanych usług niestandardowych. W bezwzględnych wartościach rejestrowanie niestandardowej usługi zwiększa opóźnienie o 0,06 ms.

Pomiar wydajności właściwości CSS, które nie są dziedziczone

Kolejne właściwości, które należy porównać, to te, które nie dziedziczą. W tym przypadku można porównywać tylko 2 rodzaje usług:

  • Zwykła usługa, która nie dziedziczy: z-index.
  • Zarejestrowana usługa niestandardowa o identyfikatorze inherits: false: --registered-no-inherit.

Niezarejestrowane usługi niestandardowe nie mogą być objęte tymi testami porównawczymi, ponieważ zawsze je dziedziczą.

Testy porównawcze

Testy porównawcze są bardzo podobne do poprzednich scenariuszy. W przypadku testu z użyciem --registered-no-inherit w fazie bootstrap testu porównawczego zostaje wstrzyknięta rejestracja tej usługi:

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

Wyniki

Wyniki tych testów porównawczych z 20 weryfikacjami na MacBooku Pro z 2021 r. (Apple M1 Pro) z 16 GB pamięci RAM daje następujące średnie wyniki:

  • Zwykła usługa, która nie dziedziczy: 290 269 wywołań na sekundę (czyli 3,44 µs na wywołanie).
  • Zarejestrowana usługa niestandardowa, która nie dziedziczy: 214 110 wywołań na sekundę (czyli 4,67 µs na wywołanie).

Test został powtórzony kilkakrotnie i wyniki były typowe.

Wykres słupkowy przedstawiający wyniki dotyczące właściwości, które nie dziedziczą. Im wyższa wartość, tym szybsze działanie.
Rysunek: wykres słupkowy przedstawiający wyniki dla właściwości, które nie dziedziczą ustawień. Im wyższa wartość, tym szybsze działanie.

Warto zauważyć, że obiekty, które nie dziedziczą, działają znacznie szybciej niż te, które dziedziczą. Jest to oczekiwane w przypadku zwykłych usług, ale dotyczy też usług niestandardowych.

  • W przypadku standardowych usług liczba uruchomień wzrosła ze 163 do ponad 290 tysięcy na sekundę, co oznacza wzrost wydajności o 1780%.
  • W przypadku właściwości niestandardowych liczba uruchomień wzrosła z 252 do ponad 214 tys. uruchomień na sekundę, co oznacza wzrost wydajności o 848%.

Najważniejszym wnioskiem jest to, że użycie parametru inherits: false podczas rejestrowania usługi niestandardowej ma znaczący wpływ. Jeśli możesz zarejestrować niestandardową usługę w usłudze inherits: false, zdecydowanie warto to zrobić.

Benchmark dodatkowy: wiele rejestracji niestandardowych usług

Innym ciekawym punktem odniesienia jest wpływ dużej liczby rejestracji niestandardowych usług. Aby to zrobić, uruchom ponownie test na platformie --registered-no-inherit,dokonując wcześniej 25 tys. rejestracji innych usług niestandardowych. Te właściwości niestandardowe są używane w :root.

Rejestracje są wykonywane w kroku setup benchmarku:

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")}
  }`);
},

Liczba wywołań na sekundę w przypadku tego testu porównawczego jest bardzo podobna do wyniku w przypadku „Zarejestrowanej niestandardowej usługi, która nie dziedziczy” (214 110 wywołań na sekundę w porównaniu z 213 158 wywołań na sekundę), ale nie jest to interesująca część testu. Należy się spodziewać, że zmiana jednej właściwości niestandardowej nie wpłynie na rejestracje z innych usług.

Ciekawym elementem tego testu jest pomiar wpływu samych rejestracji. W DevTools widać, że 25 tys. rejestracji właściwości niestandardowych ma początkowy koszt ponownego obliczania stylu wynoszący nieco ponad 30ms. Po wykonaniu tych czynności obecność tych rejestracji nie będzie miała żadnego wpływu na działanie.

Zrzut ekranu DevTools z wyróżnionym kosztem wykonania 25 tys. rejestracji usług niestandardowych w ramach funkcji „Przelicz styl”. Etykietka wskazuje, że trwało to 32,42 ms
Rys.: zrzut ekranu w Narzędziach deweloperskich z zaznaczonym kosztem wykonania 25 tys. rejestracji usług niestandardowych. Etykieta wskazuje, że zajęło to 32.42ms

Wnioski i wnioski

Podsumowując, można wyciągnąć z tego wnioski:

  • Rejestrowanie usługi niestandardowej w usłudze @property wiąże się z niewielkim spadkiem wydajności. Te koszty są często pomijalne, ponieważ rejestrując usługi niestandardowe, odblokowujesz ich pełny potencjał, którego nie można wykorzystać bez rejestracji.

  • Używanie atrybutu inherits: false do rejestrowania właściwości niestandardowej przynosi znaczące efekty. Dzięki temu zapobiegniesz dziedziczeniu właściwości. Gdy wartość właściwości się zmienia, wpływa to tylko na style dopasowanego elementu, a nie na całe drzewo podrzędne.

  • Mniej lub więcej rejestracji tagów @property nie wpływa na ponowne obliczanie stylów. Rejestracja wiąże się z bardzo niewielkim kosztem wstępnym, ale po jej zakończeniu nie musisz już nic płacić.