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 @property
benchmarkó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.
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.
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.
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ć.