Benchmarking der Leistung von CSS-Eigenschaften "@property"

Veröffentlicht: 2. Oktober 2024

Wenn Sie mit der Verwendung einer neuen Preisvergleichsportal-Funktion beginnen, sollten Sie sich mit den Auswirkungen auf die Leistung Ihrer Websites vertraut machen, unabhängig davon, ob sie positiv oder negativ sind. @property ist jetzt in der Baseline. In diesem Beitrag erfahren Sie, welche Auswirkungen das auf die Leistung hat und was Sie tun können, um negative Auswirkungen zu vermeiden.

Benchmarking der Leistung von Preisvergleichsportalen mit PerfTestRunner

Zur Leistungsmessung von CSS haben wir die Testsuite „CSS Selector Benchmark“ entwickelt. Er basiert auf der PerfTestRunner von Chromium und vergleicht die Leistungsauswirkungen von CSS. Diese PerfTestRunner wird von Blink – der zugrunde liegenden Rendering-Engine von Chromium – für interne Leistungstests verwendet.

Der Runner enthält eine measureRunsPerSecond-Methode, die für die Tests verwendet wird. Je höher die Anzahl der Ausführungen pro Sekunde, desto besser. Ein einfacher measureRunsPerSecond-Benchmark mit dieser Bibliothek sieht so aus:

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

Jede Option für measureRunsPerSecond wird durch Kommentare im Codeblock beschrieben. Die run-Funktion ist der Hauptteil, der gemessen wird.

Für CSS-Selektor-Benchmarks ist ein DOM-Baum erforderlich

Da die Leistung von CSS-Selektoren auch von der DOM-Größe abhängt, benötigen diese Benchmarks einen DOM-Baum mit angemessener Größe. Anstatt diesen DOM-Baum manuell zu erstellen, wird er generiert.

Die folgende makeTree-Funktion ist beispielsweise Teil der @property-Benchmarks. Es wird ein Baum mit 1.000 Elementen erstellt, wobei jedes Element einige verschachtelte untergeordnete Elemente hat.

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

Da die CSS-Selektor-Benchmarks den DOM-Baum nicht ändern, wird diese Baumgenerierung nur einmal ausgeführt, bevor die Benchmarks ausgeführt werden.

Benchmark ausführen

Wenn Sie einen Benchmark ausführen möchten, der Teil der Testsuite ist, müssen Sie zuerst einen Webserver starten:

npm run start

Anschließend können Sie den Benchmark unter der veröffentlichten URL aufrufen und window.startTest() manuell ausführen.

Um diese Benchmarks isoliert auszuführen, ohne dass Erweiterungen oder andere Faktoren eingreifen, wird Puppeteer über die Befehlszeile ausgelöst, um den übergebenen Benchmark zu laden und auszuführen.

Führen Sie für diese @property-Benchmarks die folgenden Befehle über die Befehlszeile aus, anstatt die entsprechende Seite unter der URL http://localhost:3000/benchmarks/at-rule/at-property.html aufzurufen:

npm run benchmark at-rule/at-property

Dadurch wird die Seite über Puppeteer geladen, window.startTest() wird automatisch aufgerufen und die Ergebnisse werden zurückgegeben.

Benchmarking der Leistung von CSS-Eigenschaften

Um die Leistung einer CSS-Eigenschaft zu messen, wird ermittelt, wie schnell sie eine Stilungültigkeit und die anschließende Stilberechnung bewältigen kann, die der Browser ausführen muss.

Bei der Stil-Ungültigmachung wird festgelegt, für welche Elemente der Stil aufgrund einer Änderung im DOM neu berechnet werden muss. Der einfachste mögliche Ansatz besteht darin, als Reaktion auf jede Änderung alles zu entwerten.

Dabei ist zwischen CSS-Eigenschaften zu unterscheiden, die vererbt werden, und solchen, die nicht vererbt werden.

  • Wenn sich eine CSS-Eigenschaft, die vererbt wird, auf ein Zielelement auswirkt, müssen sich auch die Stile aller Elemente im untergeordneten Baum unter dem Zielelement ändern.
  • Wenn eine CSS-Eigenschaft, die keine Änderungen an einem Zielelement übernimmt, ungültig wird, werden nur die Stile für dieses einzelne Element ungültig.

Da es nicht fair wäre, Properties, die übernommen werden, mit Properties zu vergleichen, die das nicht tun, müssen zwei Benchmarks ausgeführt werden:

  • Eine Reihe von Benchmarks mit übertragbaren Eigenschaften.
  • Eine Reihe von Benchmarks mit Eigenschaften, die nicht übernommen werden.

Es ist wichtig, die Unterkünfte, die Sie als Benchmark verwenden, sorgfältig auszuwählen. Einige Properties (z. B. accent-color) machen nur Stile ungültig, während viele andere (z. B. writing-mode) auch andere Dinge wie Layout oder Paint ungültig machen. Sie benötigen die Properties, die nur Stile ungültig machen.

Informationen dazu finden Sie in der Liste der CSS-Eigenschaften von Blink. Jede Property hat ein Feld invalidate, in dem aufgeführt ist, was ungültig gemacht wird.

Außerdem ist es wichtig, eine Property aus dieser Liste auszuwählen, die nicht als independent gekennzeichnet ist, da das Benchmarking einer solchen Property die Ergebnisse verfälschen würde. Unabhängige Properties haben keine Nebeneffekte auf andere Properties oder Flags. Wenn sich nur unabhängige Eigenschaften geändert haben, verwendet Blink einen schnellen Codepfad, der den Stil des untergeordneten Elements klont und die neuen Werte in dieser geklonten Kopie aktualisiert. Dieser Ansatz ist schneller als eine vollständige Neuberechnung.

Benchmarking der Leistung von CSS-Eigenschaften, die vererbt werden

Bei der ersten Gruppe von Benchmarks geht es um CSS-Properties, die erben. Es gibt drei Arten von Properties, die vererbt werden können, um sie zu testen und miteinander zu vergleichen:

  • Eine gewöhnliche Property, die Folgendes erbt: accent-color.
  • Eine nicht registrierte benutzerdefinierte Property: --unregistered.
  • Eine benutzerdefinierte Property, die bei inherits: true: --registered registriert ist.

Nicht registrierte benutzerdefinierte Properties werden dieser Liste hinzugefügt, da sie standardmäßig übernommen werden.

Wie bereits erwähnt, wurde die zu übernehmende Property sorgfältig ausgewählt, damit sie nur Stile ungültig macht und nicht als independent gekennzeichnet ist.

Bei registrierten benutzerdefinierten Properties werden nur diejenigen getestet, bei denen der inherits-Beschreibungstext auf „wahr“ gesetzt ist. Der inherits-Beschreibungstext gibt an, ob das Attribut an untergeordnete Elemente vererbt wird oder nicht. Es spielt keine Rolle, ob diese Property über CSS @property oder JavaScript CSS.registerProperty registriert ist, da die Registrierung selbst nicht Teil des Benchmarks ist.

Die Benchmarks

Wie bereits erwähnt, wird auf der Seite mit den Benchmarks zuerst ein DOM-Baum erstellt, damit die Seite genügend Knoten enthält, um die Auswirkungen der Änderungen zu sehen.

Bei jedem Benchmark wird der Wert einer Property geändert, wodurch eine Stilungültigkeit ausgelöst wird. Mit dem Benchmark wird im Grunde gemessen, wie lange die nächste Neuberechnung der Seite dauert, um alle ungültigen Stile neu zu bewerten.

Nach Abschluss eines einzelnen Benchmarks werden alle eingeschleusten Stile zurückgesetzt, damit der nächste Benchmark beginnen kann.

Der Benchmark zur Messung der Leistung beim Ändern des Stils von --registered sieht beispielsweise so aus:

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

Die Benchmarks für die anderen Property-Typen funktionieren auf die gleiche Weise, haben aber eine leere bootstrap, da keine Property registriert werden muss.

Die Ergebnisse

Wenn diese Benchmarks mit 20 Iterationen auf einem MacBook Pro (Apple M1 Pro) von 2021 mit 16 GB RAM ausgeführt werden, ergeben sich die folgenden Durchschnittswerte:

  • Regelmäßige Property, die übernommen wird (accent-color): 163 Ausführungen pro Sekunde (= 6,13 ms pro Ausführung)
  • Nicht registrierte benutzerdefinierte Property (--unregistered): 256 Ausführungen pro Sekunde (= 3,90 ms pro Ausführung)
  • Registrierte benutzerdefinierte Property mit inherits: true (--registered): 252 Ausführungen pro Sekunde (= 3,96 ms pro Ausführung)

Bei mehreren Durchläufen liefern die Benchmarks ähnliche Ergebnisse.

Balkendiagramm mit den Ergebnissen für Properties, die übernehmen. Je höher die Zahl, desto schneller ist die Leistung.
Abbildung: Balkendiagramm mit den Ergebnissen für Attribute, die übernehmen. Bei höheren Zahlen ist die Leistung schneller.

Die Ergebnisse zeigen, dass die Registrierung einer benutzerdefinierten Eigenschaft nur mit sehr geringen Kosten verbunden ist als die Registrierung der benutzerdefinierten Eigenschaft. Registrierte benutzerdefinierte Properties, die übernommen werden, werden mit 98 % der Geschwindigkeit von nicht registrierten benutzerdefinierten Properties ausgeführt. In absoluten Zahlen wird durch die Registrierung der benutzerdefinierten Eigenschaft ein Overhead von 0, 06 ms hinzugefügt.

Benchmarking der Leistung von CSS-Eigenschaften, die keine

Als Nächstes werden die Eigenschaften bewertet, die nicht übernommen werden. Hier gibt es nur zwei Arten von Unterkünften, die verglichen werden können:

  • Eine normale Property, die nicht übernommen wird: z-index.
  • Eine registrierte benutzerdefinierte Property mit inherits: false: --registered-no-inherit.

Nicht registrierte benutzerdefinierte Properties können nicht Teil dieses Benchmarks sein, da für diese Properties immer Werte übernommen werden.

Die Benchmarks

Die Benchmarks sind den vorherigen Szenarien sehr ähnlich. Für den Test mit --registered-no-inherit wird die folgende Property-Registrierung in die bootstrap-Phase der Benchmark eingefügt:

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

Die Ergebnisse

Wenn diese Benchmarks mit 20 Iterationen auf einem MacBook Pro (Apple M1 Pro) von 2021 mit 16 GB RAM ausgeführt werden, ergeben sich die folgenden Durchschnittswerte:

  • Normale Property, die nicht übernommen wird: 290.269 Ausführungen pro Sekunde (= 3,44 µs pro Ausführung)
  • Registrierte benutzerdefinierte Property, die nicht übernommen wird: 214.110 Ausführungen pro Sekunde (= 4,67 µs pro Ausführung)

Der Test wurde mehrmals wiederholt und diese Ergebnisse waren typisch.

Balkendiagramm mit den Ergebnissen für Unterkünfte, die nicht übernommen werden. Je höher die Zahl, desto schneller ist die Leistung.
Abbildung: Balkendiagramm mit den Ergebnissen für Properties, die nicht übernommen werden. Je höher die Zahl, desto schneller ist die Leistung.

Auffällig ist hier, dass Properties ohne Übernahme viel, viel schneller sind als Properties mit Übernahme. Das war bei regulären Properties zu erwarten, gilt aber auch für benutzerdefinierte Properties.

  • Bei normalen Properties stieg die Anzahl der Ausführungen von 163 Ausführungen pro Sekunde auf über 290.000 Ausführungen pro Sekunde – eine Leistungssteigerung um 1.780 %.
  • Bei benutzerdefinierten Eigenschaften ist die Anzahl der Ausführungen von 252 Ausführungen pro Sekunde auf mehr als 214.000 Ausführungen pro Sekunde gestiegen – eine Leistungssteigerung von 848 %.

Die Verwendung von inherits: false bei der Registrierung einer benutzerdefinierten Property hat also erhebliche Auswirkungen. Wenn Sie Ihre benutzerdefinierte Property bei inherits: false registrieren können, sollten Sie das unbedingt tun.

Bonusmesswert: Mehrere Registrierungen benutzerdefinierter Properties

Ein weiterer interessanter Vergleichswert ist die Auswirkung vieler Registrierungen benutzerdefinierter Properties. Führen Sie dazu den Test mit --registered-no-inherit noch einmal aus und führen Sie vorab 25.000 weitere Registrierungen benutzerdefinierter Properties durch. Diese benutzerdefinierten Properties werden auf :root verwendet.

Diese Registrierungen erfolgen im Schritt setup der Benchmark:

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

Die Ausführungsrate pro Sekunde für diesen Benchmark ist sehr ähnlich wie das Ergebnis für „Registrierte benutzerdefinierte Property, die nicht übernommen wird“ (214.110 Ausführungen pro Sekunde gegenüber 213.158 Ausführungen pro Sekunde). Das ist aber nicht der interessante Teil. Schließlich ist davon auszugehen, dass sich die Änderung einer benutzerdefinierten Property nicht auf Registrierungen aus anderen Properties auswirkt.

Das Interessante an diesem Test ist die Messung der Auswirkungen der Registrierungen selbst. In den Entwicklertools sehen Sie, dass für 25.000 Registrierungen benutzerdefinierter Eigenschaften anfänglich etwas über 30ms für die Neuberechnung von Stilen anfallen. Danach haben diese Registrierungen keine weiteren Auswirkungen.

DevTools-Screenshot mit den Kosten für die Funktion „Stil neu berechnen“ für 25.000 Registrierungen benutzerdefinierter Properties, hervorgehoben Die Kurzinfo gibt an, dass 32,42 ms benötigt wurden.
Abbildung: Screenshot der Entwicklertools mit den Kosten für die Funktion „Stil neu berechnen“ für 25.000 Registrierungen benutzerdefinierter Properties, die hervorgehoben sind. Aus der Kurzinfo geht hervor, dass es 32.42ms gedauert hat.

Fazit und Erkenntnisse

Zusammenfassend lassen sich drei Erkenntnisse daraus ziehen:

  • Die Registrierung einer benutzerdefinierten Eigenschaft mit @property ist mit geringen Leistungskosten verbunden. Diese Kosten sind oft vernachlässigbar, da Sie durch die Registrierung von benutzerdefinierten Properties ihr volles Potenzial ausschöpfen können, was ohne Registrierung nicht möglich ist.

  • Die Verwendung von inherits: false beim Registrieren einer benutzerdefinierten Property hat erhebliche Auswirkungen. Damit verhindern Sie, dass die Property übernommen wird. Wenn sich der Wert der Property ändert, wirkt sich das daher nur auf die Stile des übereinstimmenden Elements und nicht auf den gesamten untergeordneten Knoten aus.

  • Ob es wenige oder viele @property-Registrierungen gibt, hat keine Auswirkungen auf die Stilneuberechnung. Die Registrierung kostet nur sehr wenig, aber danach ist alles in Ordnung.